How to Build an API-First Frontend with OpenAPI, Orval, TanStack Query, Zod, and Next.js
Frontend development has a repetitive problem that almost every team eventually rediscovers.
You receive an API endpoint.
You write:
request functions
response types
React hooks
loading states
mutation handlers
mocks
validation logic
Then the backend changes one field.
Now:
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.
The irony is that modern teams already possess something capable of solving a large part of this problem:
the API contract.
That contract usually exists as an OpenAPI specification.
Unfortunately, many teams still treat OpenAPI as documentation.
Useful documentation.
Ignored documentation.
Static documentation.
But OpenAPI can be much more than a Swagger page living somewhere inside infrastructure documentation.
In a modern TypeScript workflow, OpenAPI can become the single source of truth powering your entire frontend integration layer.
Why API-First Development Actually Matters
Many frontend teams still work in a backend-last model.
The workflow usually looks like this:
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.
Examples:
Backend:
{
"full_name": "Jane Doe"
}Frontend expected:
{
fullName: string
}Backend:
{
"status": "pending_review"
}Frontend enum:
type Status = "draft" | "published";Runtime chaos follows.
API-First changes the order.
Instead of backend implementation being the first deliverable, the API contract becomes the first deliverable.
That means frontend developers can begin work before the backend exists.
Not with fake hand-written mocks.
Not with guessed interfaces.
With a real contract.
OpenAPI as a Development Asset — Not Documentation
An OpenAPI specification can drive far more than Swagger UI.
A modern workflow can automatically generate:
TypeScript models
API clients
React hooks
query utilities
mocks
validation schemas
documentation
testing helpers
The important shift is conceptual.
Treat OpenAPI like source code.
Version it.
Review it.
Diff it.
Generate from it.
Your repository structure might look like this:
my-app/
├── openapi/
│ └── schema.yaml
├── src/
│ ├── api/
│ ├── components/
│ ├── hooks/
│ └── app/
├── orval.config.ts
└── package.jsonKeeping specs inside Git matters.
You gain:
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.
Install dependencies:
pnpm add -D orvalCreate configuration.
orval.config.ts
import { defineConfig } from "orval";
export default defineConfig({
catalogApi: {
input: {
target:
"./openapi/schema.yaml"
},
output: {
target:
"./src/api/generated.ts",
client:
"react-query",
mode:
"single"
}
}
});Now add a script.
{
"scripts": {
"api:generate":
"orval"
}
}Run generation.
pnpm api:generateOrval now generates:
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.
Install:
pnpm add @tanstack/react-queryCreate provider setup.
providers/query-provider.tsx
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
export function AppProviders({ children }: React.PropsWithChildren) {
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
}Now generated hooks become extremely clean.
const { data, isLoading, error } = useGetProducts();No handwritten hooks.
No duplicated loading logic.
No copy-pasted fetch abstractions.
Type Safety Is Useful — But Runtime Validation Still Matters
This is where many TypeScript projects become overconfident.
Generated interfaces help.
They do not validate runtime payloads.
Your server can still return:
{
"price": "broken"
}while TypeScript confidently believes:
price: numberThis is where Zod enters the workflow.
Install:
pnpm add zodDefine schemas.
import { z }
from "zod";
export const ProductSchema =
z.object({
id: z.number(),
title: z.string(),
price: z.number()
});Runtime validation:
const result =
ProductSchema.parse(
response.data
);Now bad payloads fail loudly.
Not silently.
API-First becomes stronger when combined with runtime guarantees.
Starting Frontend Development Before Backend Exists
One of the most valuable parts of this workflow is mock generation.
This is where MSW becomes extremely useful.
Install:
pnpm add -D mswInitialize worker:
npx msw init public/Create handlers.
mocks/handlers.ts
import { http, HttpResponse } from "msw";
export const handlers = [
http.get(
"/products",
() => {
return HttpResponse.json([
{
id: 1,
title:
"Mechanical Keyboard",
price: 149
}
]);
}
)
];Browser worker:
import { setupWorker } from "msw/browser";
import { handlers } from "./handlers";
export const worker =setupWorker(
...handlers
);Start mocks during development:
await worker.start();Now frontend work continues independently from backend readiness.
This dramatically improves parallel development.
Making Generated Clients Feel Native Inside React Applications
One common criticism of API generators is that generated code often feels awkward.
You end up with:
ApiClient
.fetchUsers()or giant SDK objects nobody actually enjoys using.
The workflow becomes much cleaner once Orval generates TanStack Query hooks directly.
Suppose your OpenAPI spec contains:
paths:
/products:
get:
operationId: getProductsOrval can generate:
useGetProducts()immediately.
Now your component becomes extremely small.
"use client";
export function ProductGrid() {
const { data, isPending, error } = useGetProducts();
if (isPending) {
return <Loader />;
}
if (error) {
return (
<ErrorPanel />
);
}
return (
<section>
{data?.map(product => (
<ProductCard
key={product.id}
product={product}
/>
))}
</section>
);
}No handwritten request hooks.
No duplicated loading abstractions.
No manual cache wiring.
Mutations Become Simpler Too
Queries are only half the story.
Applications also mutate data.
Orders.
Users.
Comments.
Uploads.
Profiles.
Settings.
Traditional frontend code usually looks like this:
const saveUser = async payload => {
const response = await axios.post(
"/users",
payload
);
return response.data;
};Then:
const mutation = useMutation({
mutationFn: saveUser
});You repeat this pattern endlessly.
Generated mutation hooks remove that boilerplate.
Example:
const createOrder = useCreateOrder();Usage:
createOrder.mutate({
customerId: 42,
address:
"Berlin"
});You still control business logic.
You simply stop rewriting integration glue.
Using Zod for Safe Request Boundaries
Many teams only validate server responses.
Request validation deserves equal attention.
Forms are one of the biggest sources of invalid payloads.
Instead of trusting raw form state:
const values =form.getValues();Define schemas.
const CheckoutSchema = z.object({
email: z.email(),
quantity: z.number().min(1),
coupon: z.string().optional()
});Validation:
const payload = CheckoutSchema.parse(formData);Now invalid data fails before the request leaves the client.
That improves:
UX
debugging
API consistency
error clarity
Combining React Hook Form, Zod, and Generated APIs
This combination works extremely well in production.
Install:
pnpm add react-hook-form @hookform/resolversForm setup:
const form = useForm({
resolver:
zodResolver(CheckoutSchema)
});Submission:
const mutation = useCreateOrder();
const onSubmit = (values) => {
mutation.mutate(
values
);
};You now have:
generated API hooks
runtime validation
typed payloads
predictable mutations
without writing manual integration code.
Next.js Server Components and API-First Workflows
Modern React increasingly mixes:
client rendering
server rendering
server actions
streaming
cache boundaries
Your API workflow should fit that architecture.
Server Components work well with generated clients.
Example:
import { getProducts } from "@/api/generated";
export default async function CatalogPage() {
const products = await getProducts();
return (
<CatalogView items={products} />
);
}No client waterfall.
No hydration delay for initial fetches.
Generated clients remain usable across environments.
Working With Environment-Specific Base URLs
Real projects rarely use one API host.
You usually have:
development
staging
production
previewCreate configuration.
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL;Orval supports custom mutators.
orval.config.ts
output: { client: "react-query",
override: {
mutator: {
path: "./src/api/client.ts",
name: "useHttpClient"
}
}
}Custom client:
import axios
from "axios";
export const
httpClient = axios.create({ baseURL: API_BASE_URL });This keeps generation flexible instead of rigid.
Automating Everything in CI
API-First becomes much more powerful once generation becomes automatic.
Add scripts:
{
"scripts": {
"api:generate":
"orval",
"api:check":
"orval && git diff --exit-code"
}
}CI pipeline:
- run:
pnpm install
- run:
pnpm api:generate
- run:
pnpm buildNow contract changes become visible immediately.
Frontend drift decreases dramatically.
AI-Assisted UI Prototyping From OpenAPI Schemas
This part is still experimental.
But surprisingly useful.
Modern coding agents and LLM tooling can already generate rough UI prototypes from API contracts.
Prompt example:
Generate a responsive
React dashboard.
Use TanStack Query.
Use generated Orval hooks.
Use Zod forms.
Build CRUD screens
from the OpenAPI schema.Will this produce production UI?
Usually no.
Will it generate useful scaffolding?
Quite often yes.
Especially for:
admin panels
dashboards
CRUD flows
internal tools
prototypes
OpenAPI becomes not only a backend contract.
It becomes an input for automation across the entire frontend workflow.
Where API-First Workflows Still Have Weaknesses
This approach is powerful.
It is not magic.
Bad schemas produce bad output.
Weak contracts generate weak clients.
Common problems:
Poorly Designed Specs
Example:
type: object
additionalProperties: trueCongratulations.
Your generated types are now significantly less useful.
Schema quality matters.
Over-Generation
Not every generated artifact deserves direct use.
Sometimes you still want:
custom hooks
domain abstractions
adapter layers
feature-specific services
Generated code should support architecture.
Not replace architecture.
Runtime Drift Still Exists
Even typed clients cannot prevent:
backend bugs
invalid deployments
broken payloads
partial migrations
That is why runtime validation remains important.
Final Thoughts
Modern frontend development spends too much energy on repetitive API integration work.
Manually written clients.
Manually written types.
Manually written hooks.
Manually written mocks.
API-First workflows dramatically reduce that cost.
With a stack built around:
OpenAPI
Orval
TanStack Query
Zod
MSW
React
Next.js
you can generate large parts of the integration layer automatically while preserving strong TypeScript ergonomics.
The real advantage is not “less typing.”
The real advantage is alignment.
One contract.
One source of truth.
Less drift.
Less boilerplate.
Faster parallel development.
And fewer hours spent debugging API mismatches that should never have existed in the first place.


