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.serveis an array of middleware, so it must come beforesetup()in the sameapp.use()call. Mounting two specs on different paths requiresswaggerUi.serveFiles(spec)instead of the sharedserve, 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:
| Option | Purpose |
|---|---|
customSiteTitle | Sets the browser tab title |
customCss | Injects CSS, e.g. .topbar { display: none } to hide the bar |
customCssUrl | Loads an external stylesheet (a hosted theme) |
customfavIcon | Replaces the favicon |
explorer | Shows the spec-URL search box at the top |
swaggerOptions | Passed 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.schemasand reference them with$refinstead of repeating shapes. - Expose the raw spec at
/api-docs.jsonso generators and contract tests can consume it programmatically. - Gate
/api-docsbehind authentication (or disable it entirely) in production if the API is not public. - Use
customCssandswaggerOptionsto brand the page and set sensible defaults likedocExpansion: "none". - Pin
openapi: "3.0.3"(or 3.1) explicitly and validate the generated document in CI to catch malformed annotations early.