When your types lie: runtime validation and drift detection
Codegen, from the last post, solves a build-time problem: types generated from a spec file. This post is about a runtime problem that codegen cannot solve on its own — what happens when the live backend quietly stops matching that spec.
The gap codegen can’t close
Problem: you generated types from openapi.yaml on Monday. On Wednesday,
a backend engineer — on a different team, in a different repo, who’s never
heard of your frontend’s codegen step — ships a change that renames a field,
without touching the spec file. Your generated types still say the old field
name. TypeScript still compiles fine, because TypeScript has no idea the
backend changed; it only knows what the spec said three days ago. Your app
runs, reads response.data.userName, gets undefined, and the bug surfaces
as a blank name field in production, discovered by a support ticket instead
of a build failure.
This is what I mean by drift — the generated types and the live backend have quietly diverged, and nothing in a purely build-time codegen pipeline catches it, because codegen only ever looks at the spec file, never at what the backend actually sends.
Runtime validation — checking real responses against the spec, live
Solution: enable schema validation, and every response gets checked against its expected shape as the response arrives, not just at generation time.
createClient({
openapi: {
mode: 'runtime', // or 'codegen' — validation works with either
validateResponses: true,
},
})Response arrives
│
▼
Does it match the schema for this endpoint?
│
┌──┴──┐
yes no
│ │
▼ ▼
return ValidationError (or warn, depending on config) — see below
dataThis is a distinct mechanism from the ValidationError in the error-handling
post — that one was about your outgoing request failing 4xx validation on
the server. This is the client validating an incoming response against
what the spec said it should look like, catching the case where the backend
itself has drifted from its own documented contract.
Strict vs warn — a real tradeoff, not a default to ignore
openapi: {
validateResponses: true,
onValidationFailure: 'warn', // or 'throw'
}throw — a schema mismatch becomes a hard error immediately. Good for
staging and CI, where you genuinely want to know the moment a backend change
breaks the contract, before it ships.
warn — logs the mismatch but still returns the (possibly malformed)
data, letting your app keep running. This is what I actually run in
production. Real talk: the first time I ran throw in production, one
backend team’s minor, backward-compatible-in-practice spec drift turned into
a hard outage for an endpoint that was functionally fine — the field that
drifted wasn’t even one my UI used. warn gets you the same visibility
without turning every spec disagreement into a user-facing failure.
Drift detection — comparing spec to reality on a schedule, not per-request
Problem: per-request validation catches drift reactively, one response at a time, only for endpoints your app actually calls. It won’t tell you that an endpoint nobody’s called in production yet has already drifted — you’d only find out the first time a user hits it.
Solution: a separate drift-detection process, run on a schedule (I run mine nightly in CI), that hits your live backend’s actual endpoints and compares real responses against the committed spec — proactively, across your whole API surface, not just the parts currently in traffic.
npx api-client drift-check --spec ./openapi.yaml --baseURL https://api.example.comChecked 42 endpoints against live backend
⚠ GET /users/{id}
Spec says `role: string`
Backend actually returns `role: string | null`
⚠ POST /invoices
Spec says response includes `taxAmount`
Backend response is missing this field
✓ 40 other endpoints matchThis becomes a Slack notification in CI on my projects, not a blocking gate — drift here means “go talk to the backend team about a spec update,” not “fail the build.” Treating it as a hard CI failure just means someone eventually adds an override to skip it under deadline pressure, and then you’ve lost the signal entirely.
How this fits with codegen from the last post
Together, these two posts cover the full lifecycle of “does my frontend’s understanding of the API match reality”:
Spec file
│
├─▶ codegen (build time) → types.ts, matches spec AS OF generation
│
└─▶ drift-check (scheduled) → is spec still true, right now, against live backend?
│
▼
runtime validation (per request)
→ did THIS specific response match?Codegen answers “what did the spec say.” Drift-check answers “is the spec still accurate, across the whole API.” Runtime validation answers “did this one response, right now, actually match.” All three matter, and they answer genuinely different questions — I didn’t fully appreciate that until I’d set up all three and watched drift-check catch something runtime validation hadn’t, because nothing had hit that specific endpoint in production yet.
What sticks from this post
- Codegen only knows what the spec said at generation time — it can’t catch a backend that’s drifted since, because it never looks at live responses.
- Runtime validation checks each response against the schema as it arrives
—
warnin production to stay informed without breaking things,throwin staging/CI where you want to catch drift before it ships. - Scheduled drift-check compares your whole API surface against the live backend proactively, not just the endpoints currently receiving traffic.
- These three mechanisms answer different questions and are worth running together, not as alternatives to each other.
Next: the TanStack Query integration — React, Vue, and Solid, and how the codegen descriptor map means you’re not hand-writing query keys.
- developerehsan