Node.js · Documentation · OpenAPI

API documentation for Express, NestJS and Fastify, served by your own app

Get an OpenAPI document out of your Node.js framework, then serve documentation and a request collection from it with one line.

By · · 6 min read

Python developers who move to Node.js often miss one thing: FastAPI gives you API documentation at /docs for free, and Express gives you nothing. Node.js frameworks can produce the same OpenAPI document, but you have to ask for it, and then choose a page to show it with. This guide covers both halves: getting an OpenAPI document out of Express, NestJS or Fastify, and serving documentation plus a request collection from it with @swastik-codes/docs, a dependency-free package whose page weighs 22 KB.

The two parts of API docs

Every "Swagger page" you have seen is two things:

  1. An OpenAPI document: a JSON (or YAML) description of every endpoint, its parameters, bodies, responses and security. Your framework or a library produces it.
  2. A page that reads the document: Swagger UI, ReDoc, or the Swastik Codes pages. It is just HTML and JavaScript that fetches the document and draws it.

@swastik-codes/docs is the second part. You give it the document as an object (or a function that returns one), and it serves:

  • /swc/docs: documentation with an introduction, base URL, authentication, and every request with parameters, body fields, responses, an example response and code samples in cURL, JavaScript, Python, Go and PHP;
  • /swc/collection: a request collection where readers edit params, auth, headers and a JSON, form or file body, press Send and read the response;
  • /swc/openapi.json: the document itself, with any edits from your swastik-codes.json applied.
Shell
npm install @swastik-codes/docs

It needs Node.js 18 or later, has no dependencies, and works with CommonJS (require) and ES modules (import).

NestJS

NestJS has the most complete OpenAPI support of the Node.js frameworks: @nestjs/swagger builds the document from your controllers, DTOs and decorators. Install it if you haven't:

Shell
npm install @nestjs/swagger

Then in main.ts, build the document and hand it to @swastik-codes/docs:

TypeScript
import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { swastikCodes } from '@swastik-codes/docs';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const config = new DocumentBuilder().setTitle('Acme Shop API').setVersion('1.4.0').addBearerAuth().build();
  const document = SwaggerModule.createDocument(app, config);

  app.use(swastikCodes({ spec: document, config: './swastik-codes.json' }));
  // SwaggerModule.setup('api', app, document);   // keep Swagger UI too, if you like

  await app.listen(3000);
}
bootstrap();

Decorators fill in the document: @ApiTags('Orders') puts a controller's routes in a folder, @ApiOperation({ summary: 'Place an order' }) names a route, @ApiProperty({ example: 'maya@example.com' }) gives a DTO field an example, and @ApiBearerAuth() marks routes that need a token. With the NestJS CLI plugin ("plugins": ["@nestjs/swagger"] in nest-cli.json), DTO properties are picked up without @ApiProperty on each one.

If your Nest app runs on the Fastify adapter, register the Fastify plugin instead of app.use:

TypeScript
await app.register(require('@swastik-codes/docs').fastify, { spec: document });

Express

Express has no idea what your routes look like, so the document comes from a library. Pick the one that matches how you write code:

  • swagger-jsdoc: OpenAPI written as JSDoc comments above each route. Easy to add to an existing app.
  • zod-to-openapi (@asteasolutions/zod-to-openapi): if you validate with Zod already, register your schemas and routes and generate the document from them, so validation and docs never drift.
  • tsoa: TypeScript controllers with decorators; it generates both the routes and the document.
  • A hand-written openapi.json or openapi.yaml: perfectly fine for a small API, or one designed spec-first.

With swagger-jsdoc:

JavaScript
const express = require('express');
const swaggerJsdoc = require('swagger-jsdoc');
const { swastikCodes } = require('@swastik-codes/docs');

const spec = swaggerJsdoc({
  definition: { openapi: '3.0.3', info: { title: 'Acme Shop API', version: '1.4.0' } },
  apis: ['./routes/*.js'],
});

const app = express();
app.use(express.json());
app.use(swastikCodes({ spec, config: './swastik-codes.json' }));
app.use('/orders', require('./routes/orders'));
app.listen(3000);
JavaScript
// routes/orders.js
/**
 * @openapi
 * /orders:
 *   post:
 *     tags: [Orders]
 *     summary: Place an order
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             required: [email, lines]
 *             properties:
 *               email: { type: string, example: maya@example.com }
 *               lines: { type: array, items: { type: object, properties: { productId: { type: string }, quantity: { type: integer } } } }
 *     responses:
 *       201: { description: The order }
 */
router.post('/', placeOrder);

With a file on disk, load it once at start-up: spec: require('./openapi.json'), or parse YAML with the yaml package.

The middleware only answers paths under its prefix (/swc by default) and calls next() for everything else, so it can go anywhere in your middleware stack. Mounted inside a router (api.use(swastikCodes({ spec, prefix: '/reference' })) with app.use('/api', api)), the pages are at /api/reference/docs.

Fastify

@fastify/swagger builds the document from the JSON schemas you already attach to routes for validation and serialization. Register it first, then the Swastik Codes plugin with a function that returns the document:

JavaScript
const fastify = require('fastify')();

await fastify.register(require('@fastify/swagger'), {
  openapi: { info: { title: 'Acme Shop API', version: '1.4.0' } },
});

fastify.get('/products/:id', {
  schema: {
    tags: ['Products'],
    summary: 'Get a product',
    params: { type: 'object', properties: { id: { type: 'string' } } },
    response: { 200: { type: 'object', properties: { id: { type: 'string' }, name: { type: 'string' }, price: { type: 'number' } } } },
  },
}, async (req) => getProduct(req.params.id));

await fastify.register(require('@swastik-codes/docs').fastify, { spec: () => fastify.swagger() });
await fastify.listen({ port: 3000 });

The plugin's own routes are hidden from @fastify/swagger, so they don't appear in your API's document.

Hono, Koa and plain Node.js

JavaScript
import { hono, koa, swastikCodes } from '@swastik-codes/docs';

honoApp.use(hono({ spec }));                      // Hono, e.g. with @hono/zod-openapi's getOpenAPIDocument()
koaApp.use(koa({ spec }));                        // Koa
http.createServer(swastikCodes({ spec }));        // node:http: answers 404 for other paths

There is also swastikCodes.fetch({ spec }), a (Request) => Response | null handler for servers built on the Fetch API.

Edit the pages without touching code

Write a starter file with editor autocomplete:

Shell
npx @swastik-codes/docs init

It creates swastik-codes.json with a $schema line, so VS Code and other editors complete and check every field. Everything is optional:

JSON
{
  "$schema": "./node_modules/@swastik-codes/docs/schema.json",
  "description": "Products and orders for the Acme store.\n\n## Getting started\n\n1. Get a token from `POST /auth/token`.\n2. Send it as a bearer token.",
  "servers": [
    { "url": "https://sandbox.acme.example", "description": "Sandbox" },
    { "url": "https://api.acme.example", "description": "Production" }
  ],
  "groups": [
    { "name": "Start here", "tags": ["Authentication"] },
    { "name": "Store", "tags": ["Products", "Orders"] }
  ],
  "operations": { "POST /orders": { "summary": "Place an order" } },
  "hide": ["/internal/*", "/health"],
  "languages": ["javascript", "curl", "python"]
}

The file is re-read when it changes, so you can edit it while the server runs and reload the page. hide also removes those requests from the served openapi.json, so internal endpoints don't leak through the download link.

Things worth knowing

Requests come from the reader's browser. The collection page sends requests with fetch, so requests to the server that serves the docs always work. If readers pick a different server (production, from docs on staging), that server must allow CORS for the docs' address. Swagger UI's Try it out has the same rule.

The document is cached. If you pass a function as spec, it is called when a page loads, and the merged document is rebuilt only when the function returns a different object. Return a cached document rather than generating a new one each time.

Security headers. The two pages are sent with a strict Content-Security-Policy (scripts and styles from your server only, no inline scripts). If a proxy adds its own policy, csp: false turns ours off.

Production. The routes are public. Mount the middleware only in environments where you want docs, or protect the prefix with your existing auth middleware placed before it.

Frequently asked questions

Does it work with TypeScript?

Yes. The package ships type definitions for the options, the configuration file and every adapter, and supports both import and require.

Can I keep swagger-ui-express?

Yes. Mount both at different paths, for example Swagger UI at /api-docs and Swastik Codes at /swc, using the same document.

How big is it?

The browser downloads 22 KB (compressed) for either page, against about 427 KB for Swagger UI 5.17 in our measurement. The npm package itself is under 90 KB and installs nothing else.

Can readers open the API in an API client?

Yes. Run in Swastik Codes opens the API as a collection in Swastik Codes, and Download saves openapi.json for any other tool. The VS Code extension can also build a collection straight from your Express, NestJS or Fastify code.

Is there a Python version?

Yes: pip install swastik-codes serves the same pages from FastAPI, Flask and Django. See FastAPI documentation beyond Swagger UI.

Try it on your own API

Swastik Codes runs in the browser and is free to use. Create an account to send requests, sync your workspaces and invite your team.