Skip to content
Express.js ex api 4 min read

Documenting APIs with Swagger

A REST API is only as usable as its documentation. OpenAPI (formerly Swagger Specification) is the industry-standard, language-neutral format for describing HTTP APIs — endpoints, parameters, request bodies, responses, and auth — in a single machine-readable document. From that spec you get an interactive “try it out” UI, client SDK generators, and contract tests for free. This page shows how to write an OpenAPI spec, serve it with swagger-ui-express, generate it from JSDoc comments, and keep the docs honest as your code evolves.

Installing the tooling

Two small packages cover most needs: swagger-ui-express renders the interactive docs page, and swagger-jsdoc builds the spec from annotations in your source so it lives next to the code it describes.

npm install swagger-ui-express swagger-jsdoc

Writing an OpenAPI document

An OpenAPI document is just JSON (or YAML) with a fixed shape. The top level declares the openapi version, an info block, and a paths map keyed by URL. Reusable schemas go under components and are referenced with $ref. Here is a minimal but complete spec for a users resource.

// docs/openapi.js
module.exports = {
  openapi: "3.0.3",
  info: {
    title: "Users API",
    version: "1.0.0",
    description: "Manage application users.",
  },
  servers: [{ url: "http://localhost:3000" }],
  paths: {
    "/users/{id}": {
      get: {
        summary: "Get a user by ID",
        parameters: [
          {
            name: "id",
            in: "path",
            required: true,
            schema: { type: "integer" },
          },
        ],
        responses: {
          200: {
            description: "The requested user",
            content: {
              "application/json": {
                schema: { $ref: "#/components/schemas/User" },
              },
            },
          },
          404: { description: "User not found" },
        },
      },
    },
  },
  components: {
    schemas: {
      User: {
        type: "object",
        properties: {
          id: { type: "integer", example: 42 },
          firstName: { type: "string", example: "Ada" },
          lastName: { type: "string", example: "Lovelace" },
        },
      },
    },
  },
};

Serving the docs with swagger-ui-express

swagger-ui-express exposes the spec as an interactive page. Mount its middleware on a route — /docs is conventional — and pass the spec object. Visitors get a browsable UI with a working “Try it out” button that fires real requests against your server.

const express = require("express");
const swaggerUi = require("swagger-ui-express");
const openapiSpec = require("./docs/openapi");

const app = express();
app.use(express.json());

app.use("/docs", swaggerUi.serve, swaggerUi.setup(openapiSpec));

// Expose the raw spec so other tools can consume it
app.get("/openapi.json", (req, res) => res.json(openapiSpec));

app.listen(3000, () => console.log("Docs at http://localhost:3000/docs"));

Output: what the two routes return.

GET /docs         ->  Interactive Swagger UI (HTML)
GET /openapi.json ->  { "openapi": "3.0.3", "info": { ... }, "paths": { ... } }

In Express 5, swagger-ui-express works unchanged because app.use mounting is stable. Just keep the serve and setup middleware in that order — serve exposes the static assets, setup renders the page.

Generating the spec from JSDoc

Maintaining a separate spec file is tedious and drifts from reality. swagger-jsdoc builds the paths for you by scanning JSDoc-style @openapi comments above your route handlers, so the documentation lives beside the code.

// docs/spec.js
const swaggerJsdoc = require("swagger-jsdoc");

module.exports = swaggerJsdoc({
  definition: {
    openapi: "3.0.3",
    info: { title: "Users API", version: "1.0.0" },
  },
  apis: ["./routes/*.js"], // files to scan for annotations
});

Annotate each route with a YAML block inside a JSDoc comment. The YAML is standard OpenAPI path-item syntax.

// routes/users.js
const router = require("express").Router();

/**
 * @openapi
 * /users/{id}:
 *   get:
 *     summary: Get a user by ID
 *     parameters:
 *       - name: id
 *         in: path
 *         required: true
 *         schema: { type: integer }
 *     responses:
 *       200:
 *         description: The requested user
 *       404:
 *         description: User not found
 */
router.get("/:id", async (req, res) => {
  const user = await db.users.find(req.params.id);
  if (!user) return res.status(404).json({ error: "User not found" });
  res.json(user);
});

module.exports = router;

Feed the generated spec into the same swaggerUi.setup(spec) call and the UI updates automatically on the next restart.

Spec-first vs. code-first

The two approaches above represent the two dominant workflows. Choose based on whether the contract or the implementation leads.

ApproachHow docs are producedBest when
Code-first (swagger-jsdoc)JSDoc annotations scanned from handlersDocs should track an evolving codebase
Spec-first (hand-written openapi.js)Spec authored first, code implements itMultiple teams agree on a contract up front

Keeping docs in sync with code

Documentation that lies is worse than none. The reliable fix is to make the spec part of your tests. Validate the document against the OpenAPI schema, and use a request validator so a route that drifts from its spec fails CI rather than misleading clients.

const { validate } = require("@apidevtools/swagger-parser");

test("spec is a valid OpenAPI document", async () => {
  await validate("./openapi.json"); // throws on malformed spec
});

For runtime enforcement, libraries like express-openapi-validator reject requests and responses that violate the spec, turning your documentation into an executable contract.

Best Practices

  • Keep schemas in components and reference them with $ref so request and response shapes never drift apart.
  • Prefer code-first annotations (swagger-jsdoc) so docs move with the handler they describe instead of rotting in a separate file.
  • Serve the raw spec at /openapi.json so client generators, Postman, and contract tests can consume it programmatically.
  • Add concrete example values to schemas — the “Try it out” panel pre-fills them and makes the docs self-explanatory.
  • Validate the spec in CI and, where it matters, validate requests/responses at runtime so a drifting endpoint fails loudly.
  • Restrict or protect /docs in production if your API is private; exposing the full surface area is a reconnaissance gift to attackers.
Last updated June 14, 2026
Was this helpful?