API Documentation with Swagger and OpenAPI

Advanced
12 min

API Documentation with Swagger and OpenAPI

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.

The shape of an OpenAPI document

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) |

yaml
# 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.

Serving Swagger UI from Express

bash
npm install swagger-ui-express yaml
javascript
// 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.

Generating the spec from route comments

If you would rather keep documentation beside the handlers, swagger-jsdoc scans JSDoc blocks tagged @openapi and assembles the same document:

bash
npm install swagger-jsdoc
javascript
/** * @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.

Keeping docs and code in sync

  • Validate incoming requests against the spec with express-openapi-validator; a mismatch between docs and behaviour then fails loudly in tests instead of confusing consumers.
  • If you validate with Zod, libraries such as zod-openapi derive schemas from the same Zod objects, so one definition serves validation and documentation.
  • Protect /docs with authentication or disable it in production when the API is not public.
Quick Quiz
Question 1 of 3

What is the role of `components.schemas` in an OpenAPI document?

Key Takeaways

  • OpenAPI describes paths, operations, schemas and security in one YAML or JSON document; Swagger UI renders it interactively.
  • Serve the document with swagger-ui-express at /docs and expose the raw spec at /docs.json for tooling.
  • Use $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.
  • Keep docs honest with request validation against the spec, Zod-derived schemas, and a linter in CI.

Next lesson: Express with TypeScript — add static types to handlers, request bodies and custom req properties for safer, self-documenting code.

API Documentation with Swagger and OpenAPI - Express.js | CodeYourCraft | CodeYourCraft