Skip to content
Express.js ex libraries 4 min read

swagger-ui-express

An API is only as usable as its documentation, and hand-written docs drift out of sync the moment a route changes. swagger-ui-express mounts the Swagger UI — an interactive, browsable rendering of an OpenAPI specification — directly inside your Express app, so consumers can read and call every endpoint from the browser. Paired with swagger-jsdoc, which builds that spec from JSDoc comments living next to your route handlers, your documentation stays accurate because it ships with the code. This page covers mounting the UI, generating the spec, and customizing the result.

Mounting the UI on a spec

swagger-ui-express exposes a serve middleware that hosts Swagger UI’s static assets and a setup(spec) middleware that renders a given OpenAPI document. Mount both under a route such as /api-docs.

npm install swagger-ui-express
const express = require("express");
const swaggerUi = require("swagger-ui-express");

const app = express();

// A minimal OpenAPI 3.0 document.
const openApiSpec = {
  openapi: "3.0.3",
  info: { title: "Users API", version: "1.0.0" },
  paths: {
    "/users": {
      get: {
        summary: "List users",
        responses: {
          200: { description: "An array of users" },
        },
      },
    },
  },
};

app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(openApiSpec));

app.get("/users", (req, res) => {
  res.json([{ id: 1, name: "Ada" }]);
});

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

Visiting http://localhost:3000/api-docs renders the familiar Swagger UI, with the GET /users operation expandable and a working Try it out button.

swaggerUi.serve is an array of middleware, so it must come before setup() in the same app.use() call. Mounting two specs on different paths requires swaggerUi.serveFiles(spec) instead of the shared serve, otherwise the second route inherits the first spec.

Generating the spec from JSDoc

Writing the OpenAPI object by hand does not scale. swagger-jsdoc scans annotated comment blocks and assembles the paths and components sections for you, leaving you to define only the top-level info and servers.

npm install swagger-jsdoc
const swaggerJsdoc = require("swagger-jsdoc");

const spec = swaggerJsdoc({
  definition: {
    openapi: "3.0.3",
    info: { title: "Users API", version: "1.0.0" },
    servers: [{ url: "http://localhost:3000" }],
  },
  // Files to scan for @openapi / @swagger JSDoc blocks.
  apis: ["./routes/*.js"],
});

app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(spec));

Each route documents itself with an @openapi block. The YAML inside the comment is standard OpenAPI Path Item syntax.

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

/**
 * @openapi
 * /users:
 *   get:
 *     summary: List all users
 *     tags: [Users]
 *     responses:
 *       200:
 *         description: A list of users
 *         content:
 *           application/json:
 *             schema:
 *               type: array
 *               items:
 *                 $ref: '#/components/schemas/User'
 */
router.get("/users", async (req, res) => {
  res.json([{ id: 1, name: "Ada" }]);
});

module.exports = router;

Reusable schemas belong in a shared components block, which any path can reference with $ref.

/**
 * @openapi
 * components:
 *   schemas:
 *     User:
 *       type: object
 *       properties:
 *         id: { type: integer, example: 1 }
 *         name: { type: string, example: Ada }
 */

The generated spec object is a plain JSON document. Logging it confirms swagger-jsdoc merged your definition with the scanned annotations:

Output:

{
  "openapi": "3.0.3",
  "info": { "title": "Users API", "version": "1.0.0" },
  "paths": { "/users": { "get": { "summary": "List all users", ... } } },
  "components": { "schemas": { "User": { "type": "object", ... } } }
}

Customizing the UI

setup() accepts an options object for tweaking both the page and the embedded UI. The most common keys:

OptionPurpose
customSiteTitleSets the browser tab title
customCssInjects CSS, e.g. .topbar { display: none } to hide the bar
customCssUrlLoads an external stylesheet (a hosted theme)
customfavIconReplaces the favicon
explorerShows the spec-URL search box at the top
swaggerOptionsPassed through to Swagger UI itself (e.g. persistAuthorization)
app.use(
  "/api-docs",
  swaggerUi.serve,
  swaggerUi.setup(spec, {
    explorer: true,
    customSiteTitle: "Users API Docs",
    customCss: ".swagger-ui .topbar { background: #1a1a2e }",
    swaggerOptions: {
      persistAuthorization: true, // keep the bearer token across reloads
      docExpansion: "none",       // collapse all operations by default
    },
  })
);

It is also good practice to publish the raw spec at a stable JSON endpoint so other tools — code generators, contract tests, Postman — can consume it without scraping the UI.

app.get("/api-docs.json", (req, res) => {
  res.setHeader("Content-Type", "application/json");
  res.json(spec);
});

This setup is identical on Express 4.x and 5.x; swagger-ui-express’s middleware contract is unchanged, since both versions mount the static assets the same way under the given path.

Best Practices

  • Generate the spec from JSDoc with swagger-jsdoc so docs live beside the code and never drift.
  • Define shared models once under components.schemas and reference them with $ref instead of repeating shapes.
  • Expose the raw spec at /api-docs.json so generators and contract tests can consume it programmatically.
  • Gate /api-docs behind authentication (or disable it entirely) in production if the API is not public.
  • Use customCss and swaggerOptions to brand the page and set sensible defaults like docExpansion: "none".
  • Pin openapi: "3.0.3" (or 3.1) explicitly and validate the generated document in CI to catch malformed annotations early.
Last updated June 14, 2026
Was this helpful?