Back to Blog
APIOpenAPIExpressmiddlewareNode.js

Zero-overhead API contract validation with OpenAPI and Express

December 5, 20259 min read

How to validate every request and response against your OpenAPI spec at runtime without measurable latency cost — and why you should.

Why validate at all?

The classic argument against runtime validation is performance — "just don't, it's slow." This is outdated. Modern JSON Schema validators (ajv, zod, typebox) run in microseconds per request. The cost is negligible compared to a database query or an external API call. The benefit — catching contract violations at the boundary before they propagate — is enormous for debugging and reliability.

The approach: OpenAPI spec as the single source of truth

Instead of writing validation code by hand, I define the API contract once in an OpenAPI 3.1 spec and let tooling derive validators from it. The spec is used for: - Runtime request/response validation (ajv) - TypeScript type generation (openapi-typescript) - Documentation (Swagger UI) - Client SDK generation (openapi-generator) One spec, four consumers. Change the spec and everything stays in sync.

Wiring it into Express

The key insight is to precompile the validators at startup, not per-request. Load the spec once, walk the paths object, compile an ajv validator for each request body schema and response schema, and store them in a lookup map keyed by METHOD /path. The middleware then just does a lookup in O(1) and runs a precompiled validator. There's no schema parsing at request time — that work is already done.

Handling validation errors gracefully

When validation fails, return a structured 400 response with the ajv errors array. I wrap ajv errors into a consistent format: json { "error": "ValidationError", "issues": [ { "path": "/body/userId", "message": "must be string" } ] } This tells clients exactly what's wrong without leaking internal stack traces.

Response validation in staging

Validating responses (not just requests) catches bugs in your own code — when your handler returns a shape that doesn't match the spec. I run response validation in staging and log violations without rejecting the response, so I can fix latent bugs without impacting production. Once the spec drift is cleaned up, I flip to strict mode that returns 500 on spec violation in all environments.

Written by Luna Lancuba

← More articles