One pass turns an OpenAPI or GraphQL schema into TypeScript types, typed fetch hooks, and MSW mock handlers — with a CI drift check so the frontend can never quietly disagree with the backend. Hand-written API layers are where hallucinated fields hide; generated ones make them impossible.
Tier: Foundation · Workflow area: Component development · Tools: openapi-typescript, GraphQL Code Generator, MSW, Copilot
The API boundary is the classic source of frontend "works on my machine" bugs: a field renamed server-side, an optional the client assumed required, a mock that drifted from reality months ago. Generation from schema kills the whole category — and gives every AI tool in your repo a typed surface it cannot hallucinate against.
Generate types straight from the schema. For REST/OpenAPI:
npm i -D openapi-typescript openapi-fetch
npx openapi-typescript https://api.acme.dev/openapi.json \
-o src/api/schema.d.ts
For GraphQL, use codegen with the client preset:
npm i -D @graphql-codegen/cli @graphql-codegen/client-preset
npx graphql-codegen --config codegen.ts
Commit the generated output. Generated-and-committed beats generated-on-install: diffs become visible in PRs, which is half the value.
Build the thin typed client — generated, not hand-rolled. With openapi-fetch, the client is three lines and every call is checked against the schema:
import createClient from 'openapi-fetch';
import type { paths } from './schema';
export const api = createClient<paths>({ baseUrl: '/api' });
Now api.GET('/orders/{id}', { params: { path: { id } } }) fails to compile on a wrong path, param, or response usage. Then have AI generate the hook layer on top:
Generate TanStack Query hooks for the orders endpoints in
src/api/schema.d.ts using our api client: useOrders(filters),
useOrder(id), useArchiveOrders(). Query keys follow our
['orders', ...] convention in src/api/keys.ts. Types come ONLY from
the generated paths — no hand-written interfaces, no `any`.
Good output imports paths/component schemas for every signature and contains zero locally-declared response types.
Derive MSW handlers from the same schema. Ask for handlers that satisfy the generated types — the compiler then guarantees mocks match the contract:
Write MSW v2 handlers for GET /orders, GET /orders/{id}, and
POST /orders/archive in src/mocks/handlers/orders.ts. Response bodies
must be typed with the generated schema types (satisfies
OrdersResponse). Include: happy path with 25 realistic orders, a 422
variant for archive with field errors, a 500 variant, and a
delayed-3s variant for loading states. Export scenario helpers:
orderScenarios.error500(), orderScenarios.slow().
npm i -D msw && npx msw init public/ --save
The satisfies clause is the trick: when the schema changes, stale mocks stop compiling instead of silently lying to your tests and stories (pb-06 stories should consume these same handlers).
Add the drift check to CI. Regenerate against the live spec and fail on diff:
npx openapi-typescript $OPENAPI_URL -o /tmp/schema.d.ts
diff -q /tmp/schema.d.ts src/api/schema.d.ts \
|| { echo 'API schema drift — regenerate types'; exit 1; }
npx tsc --noEmit
The pairing matters: the diff catches the contract change; tsc shows exactly which components care. A red drift-check build is the backend telling you about a breaking change before users do.
Point every AI tool at the generated layer. One instruction-file line closes the loop (pb-02):
All API calls go through src/api (generated client + hooks). Never
hand-write fetch calls, response interfaces, or mock data shapes —
import them. If an endpoint is missing, say so; do not invent one.
The generated layer is only as honest as its source. Pin the spec URL to the environment you actually deploy against, and treat "we'll update the OpenAPI doc later" as the contract violation it is — a stale spec plus generated types produces confidently wrong code with green checkmarks.
openapi-typescript + openapi-fetch + TanStack Query + MSW; for Axios shops, openapi-typescript types with a typed wrapper still beats untyped Axios.@graphql-codegen/client-preset gives typed documents and useFragment masking; MSW mocks GraphQL operations natively (graphql.query('Orders', ...)); drift check compares the introspection result.useQuery composables) or Angular's HttpClient services generated with ng-openapi-gen; MSW works in both ecosystems unchanged.msw/native); mocks matter even more where device network debugging is painful.src/features for interface .*Response and ad-hoc fetch calls; the count should be zero.tsc fails.npm run api:gen), runnable by any teammate or agent.Generate types, client, and mocks from the schema in one pass and let CI police drift — the API boundary becomes a compile-time concern, and every AI tool in the repo inherits a typed surface it cannot hallucinate against.