Last month, a teammate renamed a field from userName to username in a single API endpoint. No tests broke. No TypeScript errors. The PR got merged on a Friday afternoon.
Monday morning, our React app was rendering blank user profiles for every customer. The frontend was reading userName — a field that no longer existed.
The problem: schema drift
API responses change shape over time. It happens in three ways, and none of them are loud about it:
A teammate refactors. They rename a field, change an integer ID to a UUID string, or make a previously-required field optional. The backend tests pass because they test backend logic, not response shape.
A database migration shifts output. You add a column, drop a column, change a default from 0 to null. The ORM happily returns the new shape. Nobody downstream knows.
A third-party API updates quietly. The weather API you depend on starts returning temperature as a string instead of a number. Their changelog? Three weeks late.
The common thread: the shape of the data changed, but nothing in your stack was watching for it.
Existing solutions (and why they fall short)
Writing Zod schemas by hand. You can manually define schemas for every endpoint, but keeping them in sync with actual responses is a full-time job. They get stale within weeks.
OpenAPI specs. Great in theory. In practice, most teams under 20 people don't maintain them, and when they do, the spec drifts from reality anyway.
End-to-end TypeScript (tRPC, ts-rest). Only works if you own both the API and the client. Doesn't help with third-party APIs or microservices in different languages.
I wanted something that learns from real traffic. No manual schemas. No spec files to maintain.
The solution: respekt
I built respekt — a Node.js middleware that observes your API responses, locks the shapes as contracts, and enforces them automatically.
Three steps:
Step 1: Observe traffic in dev/staging
import respekt from 'respekt';
app.use(respekt.observe({
routes: ['/api/*'],
sampleSize: 50,
outputDir: './contracts',
}));
The middleware silently intercepts res.json() calls. It watches 50 responses per route and records every field, type, and edge case — nullable fields, optional keys, arrays, nested objects, ISO date strings.
Step 2: Lock the contracts
npx respekt lock
This reads the traffic log, infers Zod schemas + JSON Schemas, and writes a *.schema.json contract file for each route. You commit these to git.
Step 3: Enforce in production
app.use(respekt.enforce({
contractsDir: './contracts',
onViolation: 'throw', // or 'warn' or 'log'
strict: false,
}));
Every response is now validated against the locked contract. If a field changes type, disappears, or a new unexpected field appears (in strict mode), you get a RespektViolation:
{
"route": "GET /api/users",
"violatedAt": "2026-05-13T10:22:00Z",
"violations": [
{ "field": "user.age", "expected": "number", "received": "string" },
{ "field": "user.role", "expected": "present", "received": "missing" }
]
}
The CLI also has npx respekt diff (exits with code 1 on drift — perfect for CI) and npx respekt report (prints a summary table of all monitored routes).
How it works under the hood
The inference engine samples N responses per route and builds a merged shape tree. A field that's a string in 45 samples and null in 5? z.string().nullable(). A key that's missing in 3 out of 50 responses? .optional(). ISO 8601 strings get detected automatically as z.string().datetime(). Empty arrays are flagged as z.unknown() with a warning comment until real elements are observed.
The output is both a Zod schema (as a TypeScript string you can copy into your codebase) and a standard JSON Schema used for runtime validation.
Try it out
npm install respekt zod
It's v0.1.0 — early stage but fully functional with 59 tests passing. Works with Express 4+, Express 5, and Fastify 4+. Zero runtime dependencies besides zod and micromatch.
Coming next: a native Fastify plugin (no middleware adapter needed) and a respekt watch mode for live drift monitoring.
Contributions and feedback are very welcome. If you've been bitten by silent API drift, I'd love to hear your story.
Top comments (0)