How to Build an API-First Frontend with OpenAPI, Orval, TanStack Query, Zod, and Next.js
The React Systems Newsletter
SubscribeSign in
How to Build an API-First Frontend with OpenAPI, Orval, TanStack Query, Zod, and Next.js
The React Systems Newsletter<br>May 27, 2026
Share
Frontend development has a repetitive problem that almost every team eventually rediscovers.<br>You receive an API endpoint.<br>Thanks for reading! Subscribe for free to receive new posts and support my work.
Subscribe
You write:<br>request functions
response types
React hooks
loading states
mutation handlers
mocks
validation logic
Then the backend changes one field.<br>Now:<br>generated screenshots fail
UI forms break
stale interfaces lie to TypeScript
QA opens twelve tickets
The larger the application becomes, the worse this cycle gets.<br>The irony is that modern teams already possess something capable of solving a large part of this problem:<br>the API contract.<br>That contract usually exists as an OpenAPI specification.<br>Unfortunately, many teams still treat OpenAPI as documentation.<br>Useful documentation.<br>Ignored documentation.<br>Static documentation.<br>But OpenAPI can be much more than a Swagger page living somewhere inside infrastructure documentation.<br>In a modern TypeScript workflow, OpenAPI can become the single source of truth powering your entire frontend integration layer.<br>Why API-First Development Actually Matters
Many frontend teams still work in a backend-last model.<br>The workflow usually looks like this:<br>UI design begins.
Backend implementation begins.
Frontend waits.
Backend finishes endpoints.
Frontend starts integration.
Everybody discovers mismatched assumptions.
You have probably lived through this already.<br>Examples:<br>Backend:<br>"full_name": "Jane Doe"<br>}Frontend expected:<br>fullName: string<br>}Backend:<br>"status": "pending_review"<br>}Frontend enum:<br>type Status = "draft" | "published";Runtime chaos follows.<br>API-First changes the order.<br>Instead of backend implementation being the first deliverable, the API contract becomes the first deliverable .<br>That means frontend developers can begin work before the backend exists.<br>Not with fake hand-written mocks.<br>Not with guessed interfaces.<br>With a real contract.
OpenAPI as a Development Asset — Not Documentation
An OpenAPI specification can drive far more than Swagger UI.<br>A modern workflow can automatically generate:<br>TypeScript models
API clients
React hooks
query utilities
mocks
validation schemas
documentation
testing helpers
The important shift is conceptual.<br>Treat OpenAPI like source code.<br>Version it.<br>Review it.<br>Diff it.<br>Generate from it.<br>Your repository structure might look like this:<br>my-app/<br>├── openapi/<br>│ └── schema.yaml<br>├── src/<br>│ ├── api/<br>│ ├── components/<br>│ ├── hooks/<br>│ └── app/<br>├── orval.config.ts<br>└── package.jsonKeeping specs inside Git matters.<br>You gain:<br>version history
review visibility
contract diffs
CI automation
team synchronization
Frontend engineers no longer rely on outdated screenshots of Swagger pages.
Generating a Typed Client with Orval
Instead of manually writing fetch wrappers, we will use Orval .<br>Install dependencies:<br>pnpm add -D orvalCreate configuration.<br>orval.config.ts<br>import { defineConfig } from "orval";
export default defineConfig({<br>catalogApi: {<br>input: {<br>target:<br>"./openapi/schema.yaml"<br>},
output: {<br>target:<br>"./src/api/generated.ts",
client:<br>"react-query",
mode:<br>"single"<br>});Now add a script.<br>"scripts": {<br>"api:generate":<br>"orval"<br>}Run generation.<br>pnpm api:generateOrval now generates:<br>request functions
types
TanStack Query hooks
mutation helpers
No manual API boilerplate.
Building a Real Query Layer with TanStack Query
Generated clients become significantly more useful once combined with TanStack Query.<br>Install:<br>pnpm add @tanstack/react-queryCreate provider setup.<br>providers/query-provider.tsx<br>"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
export function AppProviders({ children }: React.PropsWithChildren) {
return (
{children}
);<br>}Now generated hooks become extremely clean.<br>const { data, isLoading, error } = useGetProducts();No handwritten hooks.<br>No duplicated loading logic.<br>No copy-pasted fetch abstractions.
Type Safety Is Useful — But Runtime Validation Still Matters
This is where many TypeScript projects become overconfident.<br>Generated interfaces help.<br>They do not validate runtime payloads.<br>Your server can still return:<br>"price": "broken"<br>}while TypeScript confidently believes:<br>price: numberThis is where Zod enters the workflow.<br>Install:<br>pnpm add zodDefine schemas.<br>import { z }<br>from "zod";
export const ProductSchema =<br>z.object({<br>id: z.number(),<br>title: z.string(),<br>price: z.number()<br>});Runtime validation:<br>const result =<br>ProductSchema.parse(<br>response.data<br>);Now bad payloads fail loudly.<br>Not silently.<br>API-First becomes stronger when combined with runtime guarantees.
Starting Frontend Development Before Backend Exists
One of the most valuable parts of...