r/node • • 1d ago

Best practices for OpenAPI spec generation + validation with express.js?

I've recently started a TypeScript Express.js project and while I'm familiarizing myself, I'm struggling to know what the best way to support OpenAPI spec generation and how to streamline endpoint parameter validation.

For example, with Django & Django Rest Framework, you define your API endpoints, what they receive and return, and parameters are automatically validated and you can even generate OpenAPI specs from them with another library.

For Express, I'm seeing solutions like JSDoc, which require you to write specs in comments above function definitions, or express-openapi-validator, which require you to maintain the endpoints and spec separately, but performs the validation. I see tools for generating route code, but not vice versa.

Am I approaching this from the wrong angle? I really like having my source of truth lie in the actual code for an endpoint, with specs/validation coming from that definition, not the other way around. What packages/solutions do you recommend for this?

8 Upvotes

13 comments sorted by

3

u/aleques-itj 1d ago

Zod + express-openapi-validator + zod-openapi

You can generate the spec from your code.

Or use Hono, it will make life even easier and cleaner here. 

2

u/bill-o-more 1d ago

well, the best solution for your problem is to start a Fastify project instead of an Express one. Same concept, same DX and simplicity, but you get the API validation, typescript types to use in your server code and OpenAPI generation all from the same validation schemas (same source of truth!) out of the box. Using something like kubb afterwards you can generate client-side API helpers - TS types and typesafe functions to call your API.

Also, Matteo Collina (the author of Fastify, Node core member) published a really good agent skill to help with it.

1

u/eternal-son 1d ago

You can keep the source of truth in your code and generate the spec from it.

On Express, the setup that comes closest to DRF is to write each route's params, query, body, and response as Zod schemas. Then write a small middleware that parses each request against those schemas, so that your handler receives typed input and the middleware answers a bad request with a 400. The zod-to-openapi package from Astea Solutions builds the OpenAPI document from the same schemas once you call registerPath for each route. Because the middleware and the generator use one schema, a change to that schema changes the validation and the spec together.

If you can switch frameworks, Fastify does this without Zod. Fastify validates every request against the JSON Schema that you give its route, while Fastify's swagger plugin generates the OpenAPI document from those same schemas.

2

u/Namiastka 1d ago

I moved by hand almost all our microserives to fastify and transition was really smooth. Although my services were well tested.

Today, with llm i wouldnt hesitate a moment and make a switch.

1

u/bigujun 1d ago

Take a look at TSOA, it will generate routers, body and query validation, openapi specs all based on typescript interfaces.

1

u/Significant-Air-7864 3h ago

What worked for us after a few false starts: treat the Zod schemas as the single source of truth and generate the OpenAPI document from them, not the other way around. We validate at the boundary with a small middleware that runs safeParse against the route's schema (body, query, params separately), and on failure return a 422 with the flattened issues. Then zod-to-openapi produces the spec from those same schemas, so the docs and the runtime validation can never drift apart.

The pattern that finally made it click: each route module exports its schemas next to the handler, and a tiny registry collects them into the spec at build time. No JSDoc annotations to forget, no spec file to keep in sync by hand.

Two things I'd do differently if starting over: start CI validation of the generated spec with an OpenAPI validator from day one (catches broken examples/examples mismatches), and decide your error response envelope early — retrofitting a consistent error schema across existing endpoints is annoying.