Swagger and OpenAPI 3.1 in Node.js — API Documentation Guide 2026
Advertisement
Introduction
Why OpenAPI Documentation Matters
Undocumented APIs are black boxes. OpenAPI 3.1 provides a machine-readable contract that powers interactive Swagger UI, generates client SDKs in any language, enables API gateway configuration, and integrates with Postman collections automatically.
The best API documentation is generated from code — it stays in sync without manual updates.
Approach 1 — swagger-jsdoc (JSDoc Comments)
npm install swagger-jsdoc swagger-ui-express
npm install --save-dev @types/swagger-jsdoc @types/swagger-ui-expressimport swaggerJsdoc from 'swagger-jsdoc';
import swaggerUi from 'swagger-ui-express';
import { Express } from 'express';
const spec = swaggerJsdoc({
definition: {
openapi: '3.1.0',
info: {
title: 'My API',
version: '2.0.0',
description: 'REST API built with Node.js and TypeScript',
contact: { name: 'API Team', email: 'api@example.com' },
},
servers: [
{ url: 'https://api.example.com/v2', description: 'Production' },
{ url: 'http://localhost:3000/api', description: 'Development' },
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
},
},
security: [{ bearerAuth: [] }],
},
apis: ['./src/routes/**/*.ts', './src/schemas/**/*.ts'],
});
export function setupSwagger(app: Express) {
app.use('/docs', swaggerUi.serve, swaggerUi.setup(spec, {
customSiteTitle: 'My API Docs',
swaggerOptions: { persistAuthorization: true },
}));
app.get('/docs.json', (_req, res) => res.json(spec));
}/**
* @openapi
* /api/users:
* get:
* tags: [Users]
* summary: List all users
* description: Returns paginated list of users
* parameters:
* - in: query
* name: page
* schema: { type: integer, minimum: 1, default: 1 }
* - in: query
* name: limit
* schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
* responses:
* 200:
* description: Successful response
* content:
* application/json:
* schema:
* type: object
* properties:
* data:
* type: array
* items: { $ref: '#/components/schemas/User' }
* meta:
* $ref: '#/components/schemas/Pagination'
* 401:
* $ref: '#/components/responses/Unauthorized'
*/
router.get('/users', authenticate, getUsers);Approach 2 — Zod to OpenAPI (Type-Safe, Recommended)
Generate OpenAPI schemas directly from your Zod validation schemas — single source of truth:
npm install @asteasolutions/zod-to-openapi zodimport { extendZodWithOpenApi, OpenApiGeneratorV31, OpenAPIRegistry } from '@asteasolutions/zod-to-openapi';
import { z } from 'zod';
extendZodWithOpenApi(z);
const registry = new OpenAPIRegistry();
// Define schemas with OpenAPI metadata
const UserSchema = registry.register(
'User',
z.object({
id: z.number().openapi({ example: 1 }),
name: z.string().min(2).openapi({ example: 'Alice Smith' }),
email: z.string().email().openapi({ example: 'alice@example.com' }),
role: z.enum(['user', 'admin']).openapi({ example: 'user' }),
createdAt: z.string().datetime().openapi({ example: '2026-03-01T00:00:00Z' }),
})
);
const CreateUserSchema = registry.register(
'CreateUser',
z.object({
name: z.string().min(2).openapi({ example: 'Alice Smith' }),
email: z.string().email().openapi({ example: 'alice@example.com' }),
password: z.string().min(8).openapi({ example: 'secureP@ss1' }),
})
);
const PaginationSchema = registry.register(
'Pagination',
z.object({
total: z.number(),
page: z.number(),
limit: z.number(),
pages: z.number(),
})
);
// Register bearer auth
registry.registerComponent('securitySchemes', 'bearerAuth', {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
});
// Register endpoints
registry.registerPath({
method: 'get',
path: '/api/users',
tags: ['Users'],
summary: 'List users',
request: {
query: z.object({
page: z.string().optional().openapi({ example: '1' }),
limit: z.string().optional().openapi({ example: '20' }),
}),
},
responses: {
200: {
description: 'Paginated user list',
content: {
'application/json': {
schema: z.object({ data: z.array(UserSchema), meta: PaginationSchema }),
},
},
},
},
});
registry.registerPath({
method: 'post',
path: '/api/users',
tags: ['Users'],
summary: 'Create user',
request: { body: { content: { 'application/json': { schema: CreateUserSchema } } } },
responses: {
201: { description: 'User created', content: { 'application/json': { schema: z.object({ user: UserSchema }) } } },
409: { description: 'Email already exists' },
},
});
// Generate spec
const generator = new OpenApiGeneratorV31(registry.definitions);
export const openApiSpec = generator.generateDocument({
openapi: '3.1.0',
info: { title: 'My API', version: '2.0.0' },
servers: [{ url: 'https://api.example.com' }],
});// Mount swagger UI
import swaggerUi from 'swagger-ui-express';
import { openApiSpec } from './openapi';
app.use('/docs', swaggerUi.serve, swaggerUi.setup(openApiSpec));
app.get('/openapi.json', (_req, res) => res.json(openApiSpec));Reusing Schemas for Validation
import { z } from 'zod';
// Same schema used for OpenAPI docs AND request validation
export const createUserSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
password: z.string().min(8),
});
export type CreateUserInput = z.infer<typeof createUserSchema>;
// Route uses schema for validation
router.post('/users', async (req, res) => {
const result = createUserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ errors: result.error.flatten() });
}
const user = await userService.create(result.data);
res.status(201).json({ user });
});Common Mistakes
- Writing OpenAPI specs manually in YAML — they drift from implementation immediately
- Not documenting error responses — clients cannot handle
401,404,422without knowing the shape - Exposing
/docsin production without authentication — your internal API surface becomes public - Using
anytypes in schemas — defeats the purpose of typed documentation - Generating docs on every request — generate once at startup and cache the spec object
Best Practices
- Use
zod-to-openapito derive OpenAPI schemas from Zod schemas — single source of truth - Protect
/docsin production behind an IP allowlist or basic auth middleware - Serve
openapi.jsonat a stable URL so Postman, gateway configs, and CI tools can consume it - Add
examplevalues to all schema fields — they appear in Swagger UI and make the API self-explanatory - Run
swagger-parserin CI to validate the spec on every PR:openapi-schema-validator
Key Takeaways
- OpenAPI 3.1 is the current standard for REST API documentation — prefer it over older 3.0 or Swagger 2.0
swagger-jsdocgenerates specs from JSDoc comments;zod-to-openapiderives specs from Zod schemas- Zod-to-OpenAPI is the recommended approach in 2026 — one schema definition serves both validation and docs
- Interactive Swagger UI at
/docslets frontend teams explore endpoints without reading source code - Generated
openapi.jsonenables automatic client SDK generation in TypeScript, Python, Go, etc. - Always document error responses with shapes — not just
200 OK - Protect docs endpoints in production to avoid exposing your API surface to attackers
- Validate the generated spec in CI with a linter to catch schema errors before deployment
Advertisement
Related reading
API-First Development in 2026 — Design, Mock, Validate, Then Build6 min readType-Safe Environment Variables in 2026 — T3 Env, Zod, and Runtime Validation8 min readZod v4 — What Changed and Why It Matters for Backend Validation8 min readDocumentation as Code — Keeping API Docs Accurate and Up to Date7 min readNode.js API Best Practices 2026 — Build Production-Ready REST APIs5 min readPrisma ORM Guide 2026 — Type-Safe Database Access with PostgreSQL5 min read