The frontend breaks when the API response changes
“The backend renamed a field and the page shows undefined or crashes. TypeScript was happy because the frontend types were hand-written copies, and nobody noticed until users did.”
THE PRINCIPLE
Types copied by hand are a guess about the API. Generate them from the contract, and treat a breaking diff in that contract as a failed build.
FIRST MOVES
- Pick the source of truth: an OpenAPI or GraphQL schema committed with the backend (or a shared tRPC router type when both sides are TypeScript in one repo).
- Generate client types from it (openapi-typescript, GraphQL Code Generator) and delete hand-written response interfaces.
- In CI, regenerate and fail if the output differs from what's committed, and diff the schema against main to block breaking changes (oasdiff breaking --fail-on ERR origin/main:openapi.yaml openapi.yaml).
- Evolve additively: add fields, keep old ones until clients stop reading them, version a real break. Remember browser tabs and mobile builds run old clients for days or months.
- For separately deployed consumers, add consumer-driven contract tests (Pact) so the provider learns what clients actually read.
TOOLS: THEN → NOW
Hand-written TypeScript interfaces for API responsesTypes generated from the schema: openapi-typescript + openapi-fetch, GraphQL Code Generator
Reviewers spotting breaking changesSchema diff in CI (oasdiff) and contract tests (Pact)
REST plus copied types in a TypeScript monorepoAlso: tRPC, which shares router types with the client directly
PATTERN SNAPSHOT
# CI (shell): types must match the contract, and the contract must not break clients
npx openapi-typescript ./openapi.yaml -o ./src/api/schema.d.ts
git diff --exit-code src/api/schema.d.ts
oasdiff breaking --fail-on ERR origin/main:openapi.yaml openapi.yaml
// App code (TypeScript): a renamed field is now a type error, not a production bug
import createClient from "openapi-fetch";
import type { paths } from "./api/schema";
const api = createClient<paths>({ baseUrl: "/api/" });
const { data, error } = await api.GET("/orders/{id}", { params: { path: { id } } });CLOSE THE AI. EXPLAIN THIS.
Which contract changes are safe for clients that are already deployed, and how do you make CI catch the unsafe ones?HOW IT WORKS UNDERNEATH
RELATED SYMPTOMS
SOURCES
Guide reviewed