Generate OpenAPI Spec from Existing Routes
Walk an Express or Fastify route table, infer parameter and response types from handlers and validation schemas, and emit an OpenAPI 3.1 document for clients and docs.
Task
Produce an OpenAPI 3.1 spec that matches the live API by introspecting the route table and the validation schemas already in the code.
When to use this
The API has 40 routes but the OpenAPI spec on the docs site is six months out of date. Frontend engineers keep asking which endpoints exist. The team wants the spec generated from the code so it cannot drift.
Prerequisites
- A Node.js app with Express 4 or Fastify 4
- A validation schema library (zod, joi, or ajv) attached to each route
- The @asteasolutions/zod-to-openapi package or equivalent
Steps
1. Register an OpenAPI registry and map each route to its zod schema for params, query, and body.
registry.registerPath({
method: "post",
path: "/users",
request: { body: z.object({ email: z.string().email() }) },
responses: { 201: z.object({ id: z.string() }) },
});
Expected output: The registry holds one path entry per route, with schemas attached for every documented parameter.
2. Emit the OpenAPI document from the registry and write it to a file.
node tools/gen-openapi.mjs > openapi.jsonExpected output: An openapi.json file with openapi 3.1.0, a paths object covering every registered route, and components.schemas for reused types.
3. Validate the generated spec with a linter that catches dangling references and invalid schemas.
npx @redocly/cli lint openapi.jsonExpected output: The linter reports zero errors and zero warnings, confirming the spec is valid and self-consistent.
Troubleshooting
Problem
A route has no schema and is missing from the spec
Solution
Either attach a zod schema to the route or register it with a manual description; undocumented routes should fail the build.
Problem
The spec references a schema that is not in components
Solution
Register the shared schema with registry.registerComponent before referencing it, so zod-to-openapi emits it once.
Limitations
- Inference cannot capture response status codes that depend on runtime logic; document those manually.
- A route that uses dynamic regex paths may not map cleanly to an OpenAPI path template.
The spec should follow the code
A hand-maintained OpenAPI file always drifts from the routes. Generate it from the route table and the validation schemas that already exist, so the spec changes only when the code changes.
registry.registerPath({
method: 'post',
path: '/users',
request: { body: z.object({ email: z.string().email() }) },
responses: { 201: z.object({ id: z.string() }) },
});
Lint the output
A generated spec can still have dangling references or invalid schemas. Run a linter in CI and fail the build on any error. Treat undocumented routes as errors too, so a new endpoint cannot ship without a schema.
npx @redocly/cli lint openapi.json
Expected outcome
openapi.json covers every route, the linter passes clean, and the docs site regenerates from the spec on every build. Frontend engineers read the spec instead of asking which endpoints exist.