OpenAPI & Swagger
OpenAPI 3.x describes your HTTP API: paths, schemas, auth, and errors. Swagger UI renders that spec for humans. Node teams generate specs code-first (Zod, decorators) or write spec-first YAML then implement.
Recipe
Quick-reference recipe card - copy-paste ready.
// Code-first with @asteasolutions/zod-to-openapi (pattern)
import { OpenAPIRegistry, OpenApiGeneratorV3 } from "@asteasolutions/zod-to-openapi";
import { z } from "zod";
const registry = new OpenAPIRegistry();
const OrderSchema = z.object({ id: z.string(), status: z.string() }).openapi("Order");
registry.registerPath({
method: "post",
path: "/v1/orders",
responses: { 201: { description: "Created", content: { "application/json": { schema: OrderSchema } } } },
});
const doc = new OpenApiGeneratorV3(registry.definitions).generateDocument({
openapi: "3.0.3",
info: { title: "Orders API", version: "1.0.0" },
});# Serve UI in dev only
npm install swagger-ui-expressWhen to reach for this:
- External B2B clients need stable contracts and SDK generation
- Mobile and web teams work in parallel with backend
- CI must detect breaking path or schema changes
- Partner integrations require documented auth and webhooks
Working Example
// src/openapi/document.ts
import { z } from "zod";
import {
OpenAPIRegistry,
OpenApiGeneratorV3,
extendZodWithOpenApi,
} from "@asteasolutions/zod-to-openapi";
extendZodWithOpenApi(z);
const registry = new OpenAPIRegistry();
export const CreateOrderBody = z
.object({
customerId: z.string().uuid(),
sku: z.string(),
qty: z.number().int().positive(),
})
.openapi("CreateOrderBody");
export const Order = z
.object({
id: z.string(),
customerId: z.string().uuid(),
sku: z.string(),
qty: z.number(),
status: z.enum(["pending", "shipped"]),
})
.openapi("Order");
registry.registerPath({
method: "post",
path: "/v1/orders",
tags: ["Orders"],
request: { body: { content: { "application/json": { schema: CreateOrderBody } } } },
responses: {
201: {
description: "Order created",
content: { "application/json": { schema: z.object({ data: Order }) } },
},
400: { description: "Validation error" },
},
});
export function generateOpenApiDocument() {
return new OpenApiGeneratorV3(registry.definitions).generateDocument({
openapi: "3.0.3",
info: { title: "Acme Orders API", version: "1.0.0" },
servers: [{ url: "https://api.acme.example" }],
});
}
// src/main.ts
import express from "express";
import swaggerUi from "swagger-ui-express";
import { generateOpenApiDocument } from "./openapi/document";
const app = express();
const spec = generateOpenApiDocument();
if (process.env.NODE_ENV !== "production") {
app.use("/docs", swaggerUi.serve, swaggerUi.setup(spec));
}
app.get("/openapi.json", (_req, res) => res.json(spec));What this demonstrates:
- Zod schemas single-source for validation and OpenAPI
/openapi.jsonserves machine-readable spec- Swagger UI gated off production by default
- Paths versioned under
/v1
Deep Dive
How It Works
- Spec-first: write
openapi.yaml, implement routes to match, CI validates responses against spec - Code-first: decorators (Nest
@ApiProperty) or Zod generate YAML/JSON at build time - SDK gen:
openapi-generatorororvalfor TypeScript clients - Breaking change: required field added to response without version bump - CI should fail
Code-First Options in Node
| Tool | Framework fit |
|---|---|
@asteasolutions/zod-to-openapi | Express/Fastify + Zod |
@fastify/swagger | Fastify 5 native |
@nestjs/swagger | NestJS 11 decorators |
tsoa | Express controllers as classes |
Spec-First Workflow
1. PR updates openapi/v1.yaml
2. Reviewers comment on contract
3. Implement handlers + contract tests
4. CI: spectral lint + breaking-change diff (oasdiff)TypeScript Notes
// Share types with frontend via generated client - not raw OpenAPI dump
import type { components } from "./generated/api";
type Order = components["schemas"]["Order"];Gotchas
- Spec drift from implementation - Hand-written YAML lies. Fix: Generate from Zod or contract tests in CI.
- Swagger UI open in production - Attack surface and info disclosure. Fix: Dev/staging only or SSO.
- Examples missing - Partners guess field formats. Fix: Add
exampleon schemas. - Undocumented error shapes - Clients parse HTML error pages. Fix: Document 4xx/5xx JSON in spec.
- Giant monolithic spec - 500 paths in one file. Fix:
$refsplit files per domain module.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Postman collections | Small internal APIs | Need codegen and breaking-change CI |
| GraphQL schema | Clients need flexible queries | Public REST B2B contract required |
| README only | Private scripts | External integrators |
| protobuf/gRPC | Internal high-throughput | Browser clients need REST |
FAQs
OpenAPI 3.0 or 3.1?
3.0.3 has widest tooling support. 3.1 aligns JSON Schema closer - check generator compatibility.
Should validation match OpenAPI exactly?
Yes. Same Zod schema for runtime validation and spec generation prevents drift.
How do I hide internal routes from spec?
Register only public paths in OpenAPI registry; internal admin routes undocumented or separate spec.
Fastify swagger plugin?
@fastify/swagger + @fastify/swagger-ui with JSON Schema from route schema - idiomatic for Fastify 5.
NestJS Swagger setup?
SwaggerModule.setup in bootstrap; use DTO decorators. Export spec in CI artifact.
How to CI breaking changes?
oasdiff breaking or openapi-diff between main and PR spec artifacts.
Webhooks in OpenAPI?
OpenAPI 3.1+ webhook objects or document as async callbacks section with payload schemas.
Auth in spec?
Document bearerAuth security scheme; match middleware behavior.
Version field vs URL /v1?
info.version is API release; URL /v1 is contract version. Bump URL on breaking changes.
Can I generate spec from running server?
Some tools introspect routes - fragile. Prefer build-time generation from schemas.
Related
- API Design Basics - REST conventions
- Error Response Standards - document errors
- Versioning & Deprecation - version bumps
- Contract and Pact Tests - consumer-driven contracts
- JSON Schema Validation - Fastify schemas
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.