OpenApi - This article is part of a series.
Orval is a code generator that reads an OpenAPI (Swagger) spec and writes your frontend’s API layer for you: a typed function or React Query hook for every endpoint, TypeScript models that match the contract exactly, and, if you ask for them, MSW mock handlers filled with Faker data. You commit one config file, run orval, and run it again whenever the spec changes.
In the first post of this series I made the case for agreeing on an OpenAPI contract before anyone writes code. This one is about the tool that makes the contract actually pay off.
Hand-writing the frontend’s API layer is one of those jobs nobody admits to enjoying. You copy types out of the backend docs, write the same request wrapper for the fortieth time, and three weeks later someone renames a field and half your calls quietly break. Orval deletes that job. Point it at your OpenAPI spec and it generates the client code for you: typed functions for every endpoint, models that match the contract exactly, and mocks you can develop against before the backend exists.
Why the frontend team likes it#
The mock generation is the underrated part. Once the spec is agreed, the frontend can spin up a fake server and build the entire UI against it, instead of waiting for endpoints that are perpetually “almost done”. When the real backend lands, you point the client at it and things mostly just work, because both sides were built from the same document.
The other win is that the boring code is now correct by construction. Nobody fat-fingers a field name in a hand-rolled request body, because nobody writes a hand-rolled request body anymore.
Why the backend team likes it too#
The spec being the single source of truth changes the day-to-day conversations. Instead of chat threads asking “what does this endpoint return again?”, the frontend just reads the generated types. Backend devs get interrupted less, and they get feedback sooner: if the frontend builds against the mocks and something about the API feels awkward, you find out while it’s still cheap to change, not after it’s in production.
How it fits the workflow#
Nothing fancy. The backend proposes the spec, both teams review it, the frontend generates the client and mocks with Orval and starts building, and the backend implements the real endpoints to the same contract. When the spec changes, both sides regenerate, and the compiler tells you exactly what broke. That last part is the whole point: drift between frontend and backend becomes a compile error instead of a production bug.
Setting it up#
Install it as a dev dependency, along with whatever the generated code will import. For React Query hooks and mocks that’s TanStack Query, MSW and Faker:
npm install --save-dev orval msw @faker-js/faker
npm install @tanstack/react-queryThen add an orval.config.ts at the root of the project. This one is close to the example in Orval’s own docs:
import { defineConfig } from 'orval';
export default defineConfig({
petstore: {
input: {
target: './openapi.yaml', // a path or a URL
},
output: {
mode: 'single',
target: './src/api/petstore.ts',
schemas: './src/api/model',
client: 'react-query',
httpClient: 'fetch',
mock: true, // MSW handlers plus Faker data
override: {
fetch: { includeHttpResponseReturnType: false },
},
},
},
});client decides what you get: plain fetch or axios functions, hooks for React Query, Vue Query, Svelte Query or SWR, Angular services, or Zod schemas. The includeHttpResponseReturnType: false line makes the fetch client return the response body itself rather than { data, status }, which is usually what a component wants.
Add a script so nobody has to remember the command:
{
"scripts": {
"api": "orval"
}
}npm run api writes the hooks and models. A component then uses them like any other React Query hook, with the types already filled in:
import { useListPets } from './api/petstore';
export function PetList() {
const { data: pets, isLoading } = useListPets();
if (isLoading) return <p>Loading…</p>;
return (
<ul>
{pets?.map((pet) => <li key={pet.id}>{pet.name}</li>)}
</ul>
);
}Rename a field in the spec, run npm run api again, and pet.name turns red everywhere it’s used. That’s the drift check from above, and it costs nothing to run.
Developing against the mocks#
With mock: true, Orval also writes an MSW request handler for each endpoint, returning Faker data shaped like the response schema, plus a function that returns all of them. Hand that list to MSW’s setupWorker in the browser or setupServer in tests and the app runs against a fake backend that already matches the contract. Each handler also takes an override, so a test can return a specific payload, an empty list or an error state without anyone writing a fixture by hand.
That’s the part that lets the frontend start on day one. The mocks aren’t a separate artefact someone has to maintain; they’re regenerated from the same spec as the client, so they can’t fall behind it.
Orval vs openapi-generator, openapi-typescript and Hey API#
They all start from an OpenAPI spec, but they stop at different places.
| What it generates | Best fit | |
|---|---|---|
| Orval | Typed client functions or framework hooks (React Query, SWR, Vue, Svelte, Angular), models, MSW mocks, Zod schemas | A TypeScript frontend that wants hooks and mocks from one config |
| openapi-generator | Clients and server stubs in dozens of languages, TypeScript included | Teams that need the same spec turned into Java, Kotlin, Swift and TypeScript |
| openapi-typescript | Types only, paired with openapi-fetch for a small typed fetch wrapper | You want the types and nothing else, with almost no runtime code |
Hey API (@hey-api/openapi-ts) | A typed SDK, with plugins for TanStack Query, Zod and others | Similar ground to Orval, built around a plugin system |
If you only need types, openapi-typescript is the lightest option and it’s hard to argue with. openapi-generator earns its place when the spec has to feed more than one language; its TypeScript output works, but it reads like it was written for Java first. Between Orval and Hey API it mostly comes down to taste and which one’s output you’d rather read. I keep using Orval because the mocks come out of the same run as the hooks, and that’s the feature that changed how the two teams work together.
It’s not glamorous tooling. It just quietly removes a whole category of arguments between teams, and I’ll take that trade every time.
