Interactive Docs

Serving Interactive Documentation

Serve two things from the API itself: the raw document at a stable URL, so tooling can fetch it, and a page a human can read and click. swagger-ui-express 5.0.1 (MIT) does the second by mounting the Swagger UI 16,370 static bundle — swagger-ui-dist 5.33.0, Apache-2.0 — and injecting your document into it. swaggerUi.serve is the static middleware, swaggerUi.setup(...) builds the page, and both are needed in that order. The result lists every operation, renders each parameter from its schema, and offers a Try it out button.

Mounting the spec and the UIJavaScript
import swaggerUi from 'swagger-ui-express';
import { document } from './openapi.js';
app.get('/openapi.json', (req, res) => res.json(document));
app.use('/docs', swaggerUi.serve, swaggerUi.setup(document, {
  customSiteTitle: 'Bookshelf API', swaggerOptions: { docExpansion: 'list' }
}));

Try it out sends the request to the URL in servers, so a document naming only https://api.example.com/api/v1 cannot be exercised against a laptop; list both and allow the documentation origin in CORS (Cross-Origin Resource Sharing). Swagger UI also resolves $ref pointers against the document's own URL, so serve the file over HTTP — opening a saved copy from disk gives a resolver error instead of a page. Redoc 2.5.4 25,930 (MIT) and @scalar/express-api-reference 0.10.19 (MIT) read the same file, so switching is an afternoon, while hand-writing the page is the one option that guarantees drift.

Two rules keep it honest. Never ship it unauthenticated on a private API: it enumerates every endpoint and field you have. And make the document enforce itself — express-openapi-validator 5.6.2 (MIT) reads the file at boot and rejects requests that violate it, so an endpoint that drifts from its documentation fails a test:

Making the document the contractJavaScript
import * as OpenApiValidator from 'express-openapi-validator';
app.use(OpenApiValidator.middleware({ apiSpec: './openapi.yaml', validateResponses: true }));

Keep validateResponses to development and CI: in production it checks every response body against the schema, which costs real latency on a hot path.