Your frontend tests stub API responses with hand-written JSON fixtures. How do you make the build fail when the backend's OpenAPI or GraphQL schema renames a response field, and where does that protection stop?
answer
- the fixture should not be hand-typed
- generate from the schema, never edit
- annotate the fixture with the generated type
- regenerate in CI, fail on diff
- compile-time shape only, not values
basics
~20 sGenerate types from the API schema as a build step and annotate every fixture with the generated type, so a renamed or removed field fails typecheck. That only catches shape changes present in the schema copy you last regenerated — never runtime values or semantics.
solid answer
~50 sThe fixture has to stop being free-form JSON and start being a value of a type the frontend does not write by hand. I generate types from the published OpenAPI or GraphQL schema — `openapi-typescript` and GraphQL Code Generator both do this — and annotate each fixture or factory with the generated response type. A renamed field then fails `tsc` in the fixture file, before any test runs. The critical part is wiring: regeneration must happen in CI against the schema the service actually deploys, and the job must fail if the regenerated output differs from what is committed. Otherwise you are checking fixtures against a stale copy and feeling safe. Types stop at compile time and at shape: they cannot tell you that a documented string is now empty in practice, that a date format changed, or that a schema is wrong about its own service.
code
typescript · 15 lines// api-types.ts — generated from the API schema, never edited by hand
export type User = {
id: string;
email: string;
displayName: string;
};
// fixtures.ts — annotated with the generated type
import type { User } from './api-types';
export function makeUser(overrides: Partial<User> = {}): User {
return { id: 'u-1', email: '[email protected]', displayName: 'Ada', ...overrides };
}
// If the schema renames displayName, regeneration makes this file stop compiling.go deeper
Know that fixtures can be typed rather than free-form JSON, and that the types can be generated from the backend's schema instead of written by hand. Be able to say what a renamed field then does to the build.
Explain the pipeline end to end: fetch the schema, generate types, annotate fixtures or factories, regenerate in CI, fail the build on a diff. Say precisely which errors the compiler raises on a rename.
Show where the guarantee ends — compile time, shape only, and only as accurate as the schema itself — and describe the runtime validation or real-instance check you add on top before trusting a fully stubbed suite.
Own the question of who publishes the schema and how the generation step is enforced across many repos, including how you prevent teams from pinning stale generated types or escaping the checks with casts.
## Why a hand-written fixture is unprotected A hand-written fixture is a developer's transcription of what the API returns. Nothing connects it to the API. When the backend renames `displayName` to `name`, the fixture keeps returning `displayName`, the component keeps reading `displayName`, the assertion keeps passing, and the only thing that changed is that production is broken. The suite is not testing the app against the API; it is testing the app against a belief. Closing that gap means making the fixture's shape derive from an artifact the backend owns. ## Generating types from the schema Both common contract formats are machine-readable, and both have mature generators: - **OpenAPI** — `openapi-typescript` turns a spec into TypeScript declarations; response bodies appear under a `components['schemas'][...]` namespace. - **GraphQL** — GraphQL Code Generator reads the SDL schema plus your operation documents and emits a type per query, so the type reflects exactly the fields you selected rather than the whole schema. The generated file is build output: committed for reviewability if you like, but never edited by hand. ```typescript // generated from the API schema — do not edit export type User = { id: string; email: string; displayName: string }; // fixture, annotated with the generated type export const userFixture: User = { id: 'u-1', email: '[email protected]', displayName: 'Ada', }; ``` Rename `displayName` in the schema, regenerate, and the fixture stops compiling — an excess-property error and a missing-property error in the same file. That is the whole mechanism. ## The wiring is what actually protects you The check is only as fresh as the last generation run, so three things must be true. 1. **Regeneration is automated, not remembered.** A CI job fetches the schema from the service's published endpoint or artifact and regenerates. 2. **CI fails on a dirty diff.** Regenerate, then fail the build if the working tree changed. Without this the committed types quietly diverge and everything still passes. 3. **The schema you generate from is the deployed one.** Generating from a spec file that was itself hand-edited months ago reproduces the original problem one level up. A useful property of this setup is that the break lands in one place. If every fixture goes through a typed factory, a contract change produces a handful of compile errors in the factory module, not a scattered mess across hundreds of test files. ## Where the protection stops Be explicit about the ceiling — interviewers are usually probing for it. - **Compile time only.** A type annotation disappears at runtime. If a stub is built dynamically, read from a JSON file, or cast, nothing checks it. `as User` and `any` at the fixture boundary silently disable the entire scheme, which is why casts in fixture files deserve a lint rule. - **Shape, not values.** The type says `createdAt: string`. It cannot say the format changed from an ISO timestamp to epoch milliseconds, that a field the schema marks optional is now absent on every real response, or that an id changed from numeric-looking to a UUID and your parsing broke. - **Nullability is only as good as the schema.** OpenAPI specs are frequently wrong about which fields are nullable — often optimistic. Generated types inherit that optimism, so your fixtures never contain the `null` production sends. - **Enums that grow.** A backend adding a new status value is compatible at the type level for reading; your exhaustive `switch` still compiles against the old generated union, and the fixture never contains the new member. Only regeneration surfaces it. - **The schema can be wrong.** Nothing in this pipeline verifies that the service actually honours its own spec. A hand-maintained spec that drifted from the implementation gives you confidently typed, confidently wrong fixtures. ## What to add on top Because types stop at the compile boundary, the next layer is runtime validation of the stub payload against the schema during the test run, and a small number of tests that talk to a real instance. Types make drift cheap to detect for the class of changes a schema knows about; they are the first layer, not the whole answer. ## The GraphQL nuance With GraphQL, generated operation types describe the selection set, not the full object. That is a real advantage — your fixture only has to carry the fields the query asked for — but it means a fixture is valid only for the operation it was generated against, and copying a fixture between two queries reintroduces hand-written drift.
- If types are generated but only when someone remembers to run the script, what has actually changed?Very little. The committed types become a second hand-maintained copy of the contract, and fixtures are validated against a snapshot of unknown age. The protection comes from a CI job that regenerates against the deployed schema and fails when the output differs from what is committed.
- Name a contract change that generated types will never catch.Anything at the value level: a date format changing from ISO to epoch milliseconds, a field the spec marks optional that is now always absent, an id format change, or a new enum member the old generated union does not contain. Types describe shape, and only the shape the schema claims.
- Why does a cast in a fixture file deserve a lint rule of its own?Because a single `as` or `any` at the fixture boundary silently switches off the whole scheme for that payload. The fixture still looks typed, reviewers assume it is checked, and a schema rename passes straight through it into a green suite.
saying these in an interview costs you the question
- Claiming generated types prove the running API matches
- Casting fixtures with as any to silence the compiler
- Regenerating types by hand when someone remembers
- Assuming an OpenAPI spec is always right about nullability
- Thinking a compile-time type validates runtime payloads