An API without documentation forces every consumer, including your own frontend team, to read the source or guess. OpenAPI is the standard machine-readable description of a REST API, and Swagger UI turns it into an interactive page where anyone can read the schema and send real requests. After this lesson you will be able to write an OpenAPI 3 document, serve it from Express with Swagger UI, generate it from route comments if you prefer, and keep it truthful as the code evolves.
Four parts of an OpenAPI file matter most; write it in YAML and keep it in the repository:
| Section | Contains |
| --- | --- |
| info and servers | Title, version, base URLs |
| paths | Each URL, its operations (get, post...), parameters, request body and responses |
| components.schemas | Reusable data shapes referenced with $ref |
| components.securitySchemes | How clients authenticate (bearer JWT, cookie, API key) |
# src/docs/openapi.yaml
openapi: 3.0.3
info:
title: Todo API
version: 1.0.0
servers:
- url: /api
components:
securitySchemes:
bearerAuth: { type: http, scheme: bearer, bearerFormat: JWT }
schemas:
Todo:
type: object
required: [id, title, done]
properties:
id: { type: string }
title: { type: string, maxLength: 120 }
done: { type: boolean, default: false }
security:
- bearerAuth: []
paths:
/todos:
get:
summary: List todos
parameters:
- { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } }
responses:
"200":
description: A page of todos
content:
application/json:
schema:
type: object
properties:
data: { type: array, items: { $ref: "#/components/schemas/Todo" } }
post:
summary: Create a todo
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [title]
properties: { title: { type: string } }
responses:
"201": { description: Created, content: { application/json: { schema: { $ref: "#/components/schemas/Todo" } } } }
"422": { description: Validation failed }Top-level security applies bearer authentication to every operation; override it with security: [] on public routes such as login.
npm install swagger-ui-express yaml// src/docs/index.js
import fs from "node:fs";
import YAML from "yaml";
import swaggerUi from "swagger-ui-express";
const spec = YAML.parse(fs.readFileSync(new URL("./openapi.yaml", import.meta.url), "utf8"));
export function mountDocs(app) {
app.get("/docs.json", (req, res) => res.json(spec)); // raw spec for tooling
app.use("/docs", swaggerUi.serve, swaggerUi.setup(spec, {
swaggerOptions: { persistAuthorization: true }, // keep the token between reloads
}));
}Open http://localhost:3000/docs, click Authorize, paste a JWT, and every "Try it out" request carries the header. The /docs.json route feeds code generators and Postman imports.
If you would rather keep documentation beside the handlers, swagger-jsdoc scans JSDoc blocks tagged @openapi and assembles the same document:
npm install swagger-jsdoc/**
* @openapi
* /todos/{id}:
* get:
* summary: Get one todo
* parameters:
* - { name: id, in: path, required: true, schema: { type: string } }
* responses:
* "200": { description: The todo, content: { application/json: { schema: { $ref: "#/components/schemas/Todo" } } } }
* "404": { description: Not found }
*/
router.get("/:id", todoController.show);Build the spec with swaggerJsdoc({ definition: { openapi: "3.0.3", info: {...} }, apis: ["./src/routes/*.js", "./src/docs/components.yaml"] }) and pass the result to swaggerUi.setup() exactly as before. Comments stay close to the code at the cost of long JSDoc blocks; many teams keep components in a shared YAML file and only the per-route parts in comments.
express-openapi-validator; a mismatch between docs and behaviour then fails loudly in tests instead of confusing consumers.zod-openapi derive schemas from the same Zod objects, so one definition serves validation and documentation./docs with authentication or disable it in production when the API is not public.What is the role of `components.schemas` in an OpenAPI document?
swagger-ui-express at /docs and expose the raw spec at /docs.json for tooling.$ref to reusable schemas, global security for bearer auth, and security: [] for public operations.swagger-jsdoc builds the same spec from @openapi comments when you prefer docs next to routes.Next lesson: Express with TypeScript — add static types to handlers, request bodies and custom req properties for safer, self-documenting code.