OpenAPI & Swagger
Document your API automatically with OpenAPI specification and Swagger UI.
Why Document Your API?#
Every API has two products: the functionality itself, and the documentation. You can build the most powerful API in the world, but if developers can't figure out how to use it, they won't.
The cost of poor documentation:
- New team members take weeks instead of days to become productive
- External partners constantly ask questions your docs should answer
- Developers guess at request formats, causing support tickets
- Integration bugs from misunderstanding your API contract
Good documentation provides:
- Self-service onboarding - developers integrate without hand-holding
- Reduced support burden - docs answer common questions
- Faster development - clear contracts prevent back-and-forth
- Better adoption - developers choose well-documented APIs over alternatives
What is OpenAPI?#
OpenAPI (formerly Swagger) is the industry standard for describing REST APIs. It's a structured format that's both human-readable and machine-readable:
| Benefit | What It Enables |
|---|---|
| Machine-readable | Auto-generate client libraries, tests, mock servers |
| Human-readable | Developers understand your API at a glance |
| Interactive | Test endpoints directly in the browser |
| Industry standard | Works with thousands of tools, universal format |
| Contract-first | Define the API before implementation |
The key insight: OpenAPI isn't just documentation - it's a contract. Frontend and backend teams can work in parallel because the contract is defined upfront.
Swagger UI Example#
openapi: 3.0.0
info:
title: My API
version: 1.0.0
paths:
/users:
get:
summary: Get all users
responses:
'200':
description: List of users
This generates interactive documentation where developers can test your API directly in the browser - no Postman or curl needed.
Choosing an Approach#
There are several ways to generate OpenAPI documentation. The right choice depends on your project:
| If You... | Choose | Why |
|---|---|---|
| Have an existing Express app | swagger-jsdoc | Add docs without changing code structure |
| Use Fastify | @fastify/swagger | Auto-generates from route schemas |
| Want TypeScript-first | tsoa | Decorators generate routes AND docs |
| Already use Zod | zod-to-openapi | Single source of truth for validation + docs |
The trade-off: More automation means more constraints. swagger-jsdoc is flexible but manual. tsoa is automated but requires decorators.
Option 1: swagger-jsdoc + swagger-ui-express#
Best for: Express apps, adding docs to existing code, teams who prefer comments.
Document with JSDoc comments directly above your routes. Docs live next to the code they describe, making them easier to keep updated:
npm install swagger-jsdoc swagger-ui-express
Setup#
// src/config/swagger.js
import swaggerJsdoc from 'swagger-jsdoc';
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'My API',
version: '1.0.0',
description: 'A sample API',
},
servers: [
{ url: 'http://localhost:3000', description: 'Development' },
{ url: 'https://api.example.com', description: 'Production' },
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
},
},
},
apis: ['./src/routes/*.js'], // Path to route files
};
export const swaggerSpec = swaggerJsdoc(options);
// src/index.js
import swaggerUi from 'swagger-ui-express';
import { swaggerSpec } from './config/swagger.js';
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
app.get('/api-docs.json', (req, res) => res.json(swaggerSpec));
Document Routes#
// src/routes/users.js
/**
* @openapi
* /api/users:
* get:
* summary: Get all users
* tags: [Users]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* description: Page number
* - in: query
* name: limit
* schema:
* type: integer
* default: 20
* description: Items per page
* responses:
* 200:
* description: List of users
* content:
* application/json:
* schema:
* type: object
* properties:
* data:
* type: array
* items:
* $ref: '#/components/schemas/User'
* meta:
* $ref: '#/components/schemas/Pagination'
* 401:
* description: Unauthorized
*/
router.get('/', authenticate, getUsers);
/**
* @openapi
* /api/users/{id}:
* get:
* summary: Get user by ID
* tags: [Users]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* description: User ID
* responses:
* 200:
* description: User details
* content:
* application/json:
* schema:
* type: object
* properties:
* data:
* $ref: '#/components/schemas/User'
* 404:
* description: User not found
*/
router.get('/:id', getUser);
/**
* @openapi
* /api/users:
* post:
* summary: Create a new user
* tags: [Users]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/CreateUser'
* responses:
* 201:
* description: User created
* 400:
* description: Validation error
*/
router.post('/', createUser);
Define Schemas#
// src/routes/users.js (continued)
/**
* @openapi
* components:
* schemas:
* User:
* type: object
* properties:
* id:
* type: string
* example: "507f1f77bcf86cd799439011"
* email:
* type: string
* format: email
* example: "user@example.com"
* name:
* type: string
* example: "John Doe"
* role:
* type: string
* enum: [user, admin]
* example: "user"
* createdAt:
* type: string
* format: date-time
* CreateUser:
* type: object
* required:
* - email
* - password
* - name
* properties:
* email:
* type: string
* format: email
* password:
* type: string
* minLength: 8
* name:
* type: string
* Pagination:
* type: object
* properties:
* page:
* type: integer
* limit:
* type: integer
* total:
* type: integer
* totalPages:
* type: integer
*/
Option 2: @fastify/swagger (Fastify)#
Best for: Fastify projects, teams who want validation and docs from the same schema.
Fastify already uses JSON Schema for request/response validation. @fastify/swagger leverages these schemas to auto-generate docs - you write validation once, get documentation free:
npm install @fastify/swagger @fastify/swagger-ui
import Fastify from 'fastify';
import swagger from '@fastify/swagger';
import swaggerUi from '@fastify/swagger-ui';
const fastify = Fastify();
await fastify.register(swagger, {
openapi: {
info: {
title: 'My API',
version: '1.0.0',
},
},
});
await fastify.register(swaggerUi, {
routePrefix: '/docs',
});
// Routes with schema auto-generate docs
fastify.get('/users', {
schema: {
querystring: {
type: 'object',
properties: {
page: { type: 'integer', default: 1 },
},
},
response: {
200: {
type: 'object',
properties: {
data: {
type: 'array',
items: { $ref: 'User#' },
},
},
},
},
},
}, getUsers);
Advantage: Your validation schema IS your documentation. They can't drift apart because they're the same thing.
Option 3: tsoa (TypeScript)#
Best for: TypeScript projects, teams who want type safety everywhere.
tsoa is a batteries-included solution. You write controllers with decorators, and tsoa generates:
- Express/Koa/Hapi routes from your decorators
- OpenAPI specification from your types
- Request validation from your TypeScript types
The benefit: Your TypeScript types are the single source of truth. Change a type, and your routes, validation, and docs all update automatically.
npm install tsoa
// src/controllers/userController.ts
import { Controller, Get, Post, Body, Path, Query, Route, Tags, Security } from 'tsoa';
@Route('users')
@Tags('Users')
export class UserController extends Controller {
@Get()
public async getUsers(
@Query() page: number = 1,
@Query() limit: number = 20
): Promise<{ data: User[]; meta: Pagination }> {
return UserService.findAll({ page, limit });
}
@Get('{id}')
public async getUser(@Path() id: string): Promise<{ data: User }> {
return { data: await UserService.findById(id) };
}
@Post()
@Security('bearerAuth')
public async createUser(@Body() body: CreateUserDto): Promise<{ data: User }> {
this.setStatus(201);
return { data: await UserService.create(body) };
}
}
Trade-off: tsoa requires restructuring your code to use its controller pattern. Great for new projects, more effort for existing ones.
Option 4: Zod to OpenAPI#
Best for: Projects already using Zod for validation, avoiding duplication.
If you're using Zod for request validation (which you should - see our Zod guide), you're already defining what valid data looks like. Why define it again for OpenAPI? zod-to-openapi generates OpenAPI schemas from your Zod schemas:
npm install @asteasolutions/zod-to-openapi
import { z } from 'zod';
import { extendZodWithOpenApi, OpenAPIRegistry } from '@asteasolutions/zod-to-openapi';
extendZodWithOpenApi(z);
const registry = new OpenAPIRegistry();
// Define schema with OpenAPI metadata
const UserSchema = z.object({
id: z.string().openapi({ example: '123' }),
email: z.string().email().openapi({ example: 'user@example.com' }),
name: z.string().openapi({ example: 'John Doe' }),
}).openapi('User');
// Register endpoint
registry.registerPath({
method: 'get',
path: '/api/users/{id}',
summary: 'Get user by ID',
request: {
params: z.object({ id: z.string() }),
},
responses: {
200: {
description: 'User found',
content: {
'application/json': {
schema: z.object({ data: UserSchema }),
},
},
},
},
});
// Generate OpenAPI spec
const generator = new OpenApiGeneratorV3(registry.definitions);
const spec = generator.generateDocument({
openapi: '3.0.0',
info: { title: 'My API', version: '1.0.0' },
});
The pattern: Define validation schemas with Zod, extend them with .openapi() metadata, and generate docs. One definition serves validation, TypeScript types, and documentation.
Best Practices#
1. Group Endpoints with Tags#
Tags organize your API into logical sections. Without them, developers see a flat list of 50+ endpoints. With tags, they see "Users", "Products", "Orders" - instantly navigable.
/**
* @openapi
* tags:
* - name: Users
* description: User management
* - name: Auth
* description: Authentication
* - name: Products
* description: Product catalog
*/
2. Document Error Responses#
Developers need to know what can go wrong, not just what happens when everything works. Document error responses so clients can handle them gracefully:
/**
* @openapi
* components:
* responses:
* NotFound:
* description: Resource not found
* content:
* application/json:
* schema:
* type: object
* properties:
* error:
* type: object
* properties:
* message:
* type: string
* code:
* type: string
* ValidationError:
* description: Validation failed
* Unauthorized:
* description: Authentication required
*/
3. Provide Realistic Examples#
Examples are often more useful than schema definitions. A developer can glance at an example and immediately understand the format:
schema:
type: object
example:
data:
id: "507f1f77bcf86cd799439011"
email: "user@example.com"
name: "John Doe"
role: "user"
createdAt: "2024-01-15T10:30:00Z"
Tips for good examples:
- Use realistic values (not "string" or "test")
- Show complete responses, not fragments
- Include edge cases (empty arrays, null fields)
- Match the examples to what your API actually returns
Comparison#
| Tool | Approach | TypeScript | Best For |
|---|---|---|---|
| swagger-jsdoc | JSDoc comments | Optional | Express, existing code |
| @fastify/swagger | Schema validation | Optional | Fastify projects |
| tsoa | Decorators | Required | TypeScript-first |
| zod-to-openapi | Zod schemas | Works great | Already using Zod |
Key Takeaways#
-
Documentation is a product - Treat it with the same care as your code. Bad docs mean bad adoption.
-
OpenAPI is the standard - It's not just docs, it's a contract. Use it for all APIs, internal or public.
-
Generate from code - Manual docs drift out of date. Generate from schemas, types, or decorators so they can't lie.
-
Include realistic examples - Examples are often more useful than schemas. Show what responses actually look like.
-
Document what can go wrong - Happy path documentation isn't enough. Show error responses so clients can handle them.
-
Choose based on your stack - Already using Zod? Use zod-to-openapi. TypeScript purist? Use tsoa. Existing Express app? Use swagger-jsdoc.
The Goal
Your API documentation should be so good that developers can integrate without asking you a single question. If you're answering the same questions repeatedly, your docs are missing something.
Ready to level up your skills?
Explore more guides and tutorials to deepen your understanding and become a better developer.