A type-safe client from OpenAPI
OpenAPI (formerly Swagger) describes HTTP APIs in a machine-readable schema. If your team maintains openapi.yaml, you can generate TypeScript types—and optionally fetch wrappers, mocks, and React Query hooks—from that single source of truth.
This post compares two popular generators, shows how I wire CI, and where runtime validation still belongs.
Layer 1: Types only with openapi-typescript
openapi-typescript emits a declaration file describing paths, components.schemas, and operation parameters/responses. It does not fetch; it only types.
npx openapi-typescript ./openapi/openapi.yaml -o ./src/generated/api.d.ts
Usage pattern:
import type { paths } from './generated/api';
type GetUserResponse =
paths['/users/{id}']['get']['responses']['200']['content']['application/json'];
You still write fetch or axios, but wrong paths, methods, or JSON shapes become compile errors when the schema changes.
Versioning the spec
Commit either:
- The generated
.d.ts(simplest for small teams), or - Only the YAML + generate in CI (avoids merge noise, requires deterministic codegen).
Pick one policy and document it in CONTRIBUTING.md.
Layer 2: Orval for clients and React Query
Orval reads OpenAPI and outputs:
- Typed functions (
getUser,listPosts, …) - Optional React Query hooks with keys derived from parameters
- Optional MSW mocks for tests
This saves hundreds of lines of hand-written hooks—but adds build complexity. I use Orval when:
- The app has many endpoints consumed from client components.
- Query keys must stay consistent across the codebase.
Runtime validation still matters
OpenAPI describes intent, not runtime truth. Servers can return malformed JSON, proxies can strip fields, and mobile clients might send old payloads.
I combine:
- Compile-time: generated types for happy path.
- Runtime:
zodschemas for external boundaries (webhooks, third-party callbacks, admin imports).
import { z } from 'zod';
const User = z.object({
id: z.string().uuid(),
displayName: z.string().min(1),
});
type User = z.infer<typeof User>;
You can generate Zod from OpenAPI with community tools; validate the output before trusting it in production.
CI: fail on drift
Add a job step:
- name: Generate API types
run: pnpm run codegen:api
- name: Check for uncommitted changes
run: git diff --exit-code
If someone changes the backend contract without regenerating the client, the PR goes red.
Error modeling
HTTP errors are not in the happy-path type. I wrap calls in a small helper:
type ApiResult<T> =
| { ok: true; data: T }
| { ok: false; status: number; body: unknown };
Typed data on success; log body on failure; optionally parse problem+json.
Summary
OpenAPI → TypeScript closes a whole class of integration bugs. Choose openapi-typescript when you only need types, Orval when you want hooks and mocks. Always plan for runtime validation at trust boundaries—and enforce regeneration in CI so the contract never silently diverges.