Express has no built-in OpenAPI output — unlike FastAPI or NestJS, there's no single official module to add. Pick one of three real approaches (annotate routes with swagger-jsdoc, generate from decorated controllers with tsoa, or derive the spec from Zod schemas with zod-openapi), get a spec out, then turn it into a TypeScript or Python SDK with a generator or a hosted pipeline like Sourced.
Three ways to get OpenAPI out of Express
Option 1: swagger-jsdoc — annotate existing routes
swagger-jsdoc reads JSDoc comment blocks in your source files and assembles them into an OpenAPI document. It's the lowest-friction option if you already have Express routes and don't want to restructure them.
npm install swagger-jsdoc --save
const swaggerJsdoc = require('swagger-jsdoc');
const options = {
definition: {
openapi: '3.0.0',
info: { title: 'Hello World', version: '1.0.0' },
},
apis: ['./src/routes/*.js'], // glob pointing at annotated files
};
const openapiSpecification = swaggerJsdoc(options);
Each route gets its own @openapi (or @swagger) YAML block directly above the handler. Serve the resulting object with swagger-ui-express for an interactive UI, or write it to a file for a generator.
Option 2: tsoa — generate from decorated TypeScript controllers
tsoa flips the direction: instead of annotating existing routes, you write TypeScript controllers with decorators, and tsoa generates both the OpenAPI spec and the Express routing code from them.
yarn add tsoa express
yarn add -D typescript @types/node @types/express
A tsoa.json config file controls output:
{
"entryFile": "src/app.ts",
"noImplicitAdditionalProperties": "throw-on-extras",
"controllerPathGlobs": ["src/**/*Controller.ts"],
"spec": { "outputDirectory": "build", "specVersion": 3 },
"routes": { "routesDir": "build" }
}
@Route("users")
export class UsersController extends Controller {
@Get("{userId}")
public async getUser(@Path() userId: number): Promise<User> {
return new UsersService().get(userId);
}
}
Then generate both the spec and the route bindings:
yarn run tsoa spec-and-routes
The TypeScript types are the source of truth here — you don't hand-write YAML, and the spec can't drift from the code the way swagger-jsdoc annotations can.
Option 3: zod-openapi — derive the spec from runtime validation schemas
zod-openapi goes a step further: if you already validate requests and responses with Zod, it extends your existing schemas with OpenAPI metadata instead of adding a second definition of your data shapes.
npm install zod zod-openapi
import * as z from 'zod/v4';
import { createDocument } from 'zod-openapi';
const jobId = z.string().meta({
description: 'A unique identifier for a job',
example: '12345',
id: 'jobId',
});
const document = createDocument({
openapi: '3.1.0',
info: { title: 'My API', version: '1.0.0' },
paths: {
'/jobs/{jobId}': {
get: {
requestParams: { path: z.object({ jobId }) },
responses: {
'200': {
description: 'OK',
content: { 'application/json': { schema: z.object({ jobId }) } },
},
},
},
},
},
});
The .meta({ id: 'jobId' }) call is what gives that schema a stable name in the output document — the same schema used across multiple routes stays one named type instead of being redefined per endpoint.
Which one should you pick?
| Situation | Best fit | Why |
|---|---|---|
| Existing Express app, JS or TS, no time to restructure | swagger-jsdoc |
Annotate in place, no new source-of-truth |
| New service, TypeScript, want strict typed controllers | tsoa |
Spec and route code both generated from one decorated source |
| Already using Zod for request/response validation | zod-openapi |
No second schema definition to keep in sync |
| Want the smallest possible dependency footprint | swagger-jsdoc |
No decorators, no build step, plain JS works |
| Want the spec impossible to drift from the code | tsoa or zod-openapi |
Both derive the spec from the same source the server runs |
The gotcha specific to Express: nothing enforces a source of truth
Every other framework in this series (FastAPI, NestJS, Django) has one canonical path from code to spec. Express doesn't — and that flexibility causes the most common Express-specific codegen problem: a swagger-jsdoc comment block that's stale relative to the route it documents, because nothing fails the build when they diverge. A route handler gets a new parameter; the JSDoc block above it doesn't. Nothing errors. The spec silently lies.
The most common symptom is a missing or copy-pasted operationId — easy to skip in a hand-written YAML block, or duplicate by copying a comment from a similar route. See fixing missing operationId errors for what that produces in generated SDK method names. tsoa and zod-openapi narrow this risk rather than eliminate it: the spec is derived from the same types the server enforces at runtime, so a behavior change is far more likely to also change the spec. With zod-openapi specifically, a schema with no .meta({ id: ... }) has nothing to reuse as a name — the same anonymous-schema symptom covered in fixing inline schema names like Type1 and InlineObject. Always set id on any Zod schema you expect as a real type in the generated SDK.
From an Express spec to hosted docs and typed SDKs
Whichever tool produced your openapi.json, turning it into an installable SDK is the same next step — and with docs and SDK previews together, it's the shortest path from OpenAPI spec to live docs and SDK previews. With Sourced:
- Upload the spec or connect the repo. Connect from GitHub, or paste the file straight from
swagger-jsdoc,tsoa, orzod-openapi's output. - Preview TypeScript and Python SDKs. Method names, types, and error classes render in the dashboard before anything publishes — a stale or duplicated
operationIdshows up here, not in a customer's install. - Get hosted docs and llms.txt from the same spec. No separate docs deploy needed.
- Publish when the preview is clean. Free unlimited previews and up to 2 hosted noindex docs review sites and one hosted MCP server, no credit card.
OSS generators for other languages
Sourced's generated SDKs focus on TypeScript and Python today — the two ecosystems where teams publish first, and where an Express/Node API's own consumers usually already are. Whichever of the three Express approaches you use, the resulting spec also works with the standard per-language OpenAPI generators for anything further out — Go, Java, Ruby, PHP, C#, Kotlin, Rust, and Swift each have a dedicated walkthrough. The five-minute generate-an-SDK guide covers running a local generator against any of these outputs.
Honest scope: when hand-writing wins
For a small internal Express API with two or three consumers who can read the route file directly, skip all three tools — a short README with curl examples costs less than a spec pipeline nobody asked for. swagger-jsdoc earns its keep once you have external consumers but can't justify restructuring routes. Reach for tsoa or zod-openapi when starting fresh or already migrating to stricter typing — retrofitting either onto a large existing codebase is a real project, not an afternoon.
FAQ
Does Express generate OpenAPI automatically?
No. Express has no built-in OpenAPI support. You need a library — swagger-jsdoc, tsoa, or zod-openapi are the three current options — to produce a spec from your routes.
What's the difference between swagger-jsdoc and tsoa?
swagger-jsdoc reads YAML comments you write above existing route handlers and assembles them into a spec — the code and the spec are two separate things you keep in sync by hand. tsoa generates both the spec and the Express routing code from decorated TypeScript controllers, so there's one source of truth instead of two.
Do I need TypeScript to use tsoa or zod-openapi?
Yes for both. tsoa is built entirely around TypeScript decorators and compile-time types. zod-openapi technically works with Zod in a JavaScript project, but you lose most of the benefit — the value is in Zod's inferred TypeScript types staying in sync with the generated spec.
Can I use more than one of these together, or does one produce a cleaner SDK?
Pick one per project — mixing swagger-jsdoc annotations on some routes and tsoa controllers on others reintroduces the drift problem both tools otherwise solve. tsoa and zod-openapi tend to produce cleaner downstream SDKs than swagger-jsdoc because the spec is derived from enforced types rather than hand-written comments, which cuts down on missing operationIds and mismatched parameters — swagger-jsdoc can match that, but only with discipline, since nothing in the tool itself prevents drift.
Does the SDK generator care which of the three I used?
No. Once you have a valid OpenAPI document, a generator (local or hosted) doesn't know or care which tool produced it — validate the output spec itself, not the source.
Ship it
Whichever path gets you a clean openapi.json out of Express, start free on Sourced to preview a TypeScript and Python SDK plus hosted docs from it in one pass, or run the spec through the OpenAPI validator first to catch drift and naming gaps before you generate anything.