Quickstart
Full install-to-first-request walkthrough for greenfield projects.
openapi-fetch solved a real problem: a type-safe client that wraps native fetch without adding
axios. If you used it, you know the value. The reasons to migrate now are:
openapi-fetch types your responses at compile time but does not check them at runtime. If an API returns an unexpected shape, you get a silent type error at runtime, not a clean failure at the boundary.openapi-zod-ts toolchain generates Zod v4 schemas from the same spec and wires them into every fetch call. Runtime validation comes for free when you add one config line.openapi-fetch has no server-side counterpart. openapi-zod-ts ships @codewithagents/openapi-server which generates a typed service interface (and optional Fastify, Hono, or Express router) from the same spec, types both ends of the wire.// Install: npm install openapi-fetch// Manual type definition file (or generated by openapi-typescript):import createClient from 'openapi-fetch'import type { paths } from './openapi-types' // generated by openapi-typescript
const client = createClient<paths>({ baseUrl: 'https://api.example.com' })
// To attach auth you wrap each call or use middleware:client.use({ onRequest({ request }) { request.headers.set('Authorization', `Bearer ${getToken()}`) return request },})// Install: npm install -D openapi-zod-ts// Run: npx openapi-zod-ts (generates client.ts, models.ts, etc.)import { configureClient } from './src/api/client-config'
// Call once at app startup. token can be sync or async, called per request.configureClient({ baseUrl: 'https://api.example.com', token: () => getToken(),})No middleware pattern needed. The token field accepts a function called before every
request, so the bearer token stays fresh without any per-call boilerplate.
// openapi-fetch style: path string, generic runtime objectconst { data, error } = await client.GET('/tasks/{taskId}', { params: { path: { taskId: '123' } },})
// `data` is typed but NOT validated at runtime.// If the server returns an unexpected shape, TypeScript does not know.if (error) throw errorconsole.log(data.title)// Generated function, fully typed, no path strings:import { getTask } from './src/api'
const task = await getTask('123')// Throws ApiError on non-2xx, throws ZodError if the response shape is wrong// (when input_schema is set in config).console.log(task.title)The generated function name mirrors the operationId. Paths and parameter types come from
the spec, not from a generic path string.
const { data, error } = await client.POST('/tasks', { body: { title: 'New task', status: 'open' },})if (error) throw errorimport { createTask } from './src/api'
const task = await createTask({ title: 'New task', status: 'open' })// With input_schema: request body is validated against TaskSchema before the fetch.// Bad data throws immediately, before any network call.// Install: npm install @openapi-ts/react-query (maintenance mode)import { useQuery } from '@openapi-ts/react-query'
// Hooks wrap the openapi-fetch client, path strings again:function TaskList() { const { data, isPending } = useQuery({ client, path: '/tasks', method: 'GET', }) // ...}// Install: npm install -D @codewithagents/openapi-react-query// Run: npx openapi-react-query (generates hooks.ts)import { useListTasks } from './src/api/hooks'
function TaskList() { const { data, isPending } = useListTasks() // Typed from the generated client, no path strings, no wrapper needed.}Hooks are generated once from the spec. Every hook name, parameter type, and return type is derived from your operations, not from a generic path string at runtime.
import { useMutation } from '@openapi-ts/react-query'
function CreateTaskButton() { const { mutate } = useMutation({ client, path: '/tasks', method: 'POST', }) // ...}import { useCreateTask } from './src/api/hooks'
function CreateTaskButton() { const { mutate } = useCreateTask({ onSuccess: (data) => console.log('created', data.id), }) // mutate({ title: 'New task', status: 'open' }) -- fully typed}openapi-fetch has no runtime validation story. Types are erased at runtime, so a server that
returns an extra field, a missing required field, or a wrong type goes undetected until it
surfaces as a UI bug three components deep.
With openapi-zod-ts, adding one field to your config bootstraps a Zod v4 schema file:
{ "input_openapi": "./openapi.json", "output": "./src/api", "input_schema": "./src/api/schemas.ts"}The first npx openapi-zod-ts run writes schemas.ts. Every subsequent run skips it, so the
file is yours to extend. The generated client.ts already calls Schema.parse(body) before
every request and Schema.parse(await res.json()) after every response:
// client.ts (auto-generated when input_schema is set)export async function getTask(id: string): Promise<Task> { const res = await _request('GET', `/tasks/${id}`) return TaskSchema.parse(await res.json()) // throws ZodError on bad shape}
export async function createTask(body: TaskWritable): Promise<Task> { TaskWritableSchema.parse(body) // validates before sending const res = await _request('POST', '/tasks', { body }) return TaskSchema.parse(await res.json())}Once you own schemas.ts, you can add refinements that no spec can express:
// src/api/schemas.ts (yours, never overwritten)export const TaskSchema = z.object({ id: z.string().uuid(), title: z.string().min(1, 'Title is required'), status: z.enum(['open', 'done']), due_date: z.string().datetime().optional(),})Remove openapi-fetch and openapi-typescript.
npm uninstall openapi-fetch openapi-typescript @openapi-ts/react-queryDelete any generated openapi-types.ts and paths.d.ts files.
Install the new generator.
npm install -D openapi-zod-tsFor React Query hooks:
npm install -D @codewithagents/openapi-react-querynpm install @tanstack/react-query # already installed if you used openapi-react-queryCreate the config file.
{ "input_openapi": "./openapi.json", "output": "./src/api", "input_schema": "./src/api/schemas.ts"}input_schema is optional, but strongly recommended. It gives you Zod v4 runtime
validation on every request and response, with a schema file you control and can extend.
Run the generators.
npx openapi-zod-tsnpx openapi-react-queryOr add to package.json:
{ "scripts": { "generate": "openapi-zod-ts && openapi-react-query" }}Replace the client setup.
Remove the createClient call and replace with configureClient:
// Remove:// import createClient from 'openapi-fetch'// const client = createClient<paths>({ baseUrl: ... })
// Add (src/main.ts or App.tsx):import { configureClient } from './src/api/client-config'configureClient({ baseUrl: 'https://api.example.com', token: () => getToken() })Replace path-string calls with generated functions.
Find every client.GET(...), client.POST(...), etc. and replace with the corresponding
generated function. The function name matches the operationId from the spec.
// Before:const { data } = await client.GET('/tasks/{taskId}', { params: { path: { taskId } } })
// After:const data = await getTask(taskId)Replace hook usage.
If you used @openapi-ts/react-query, replace each hook call with the generated equivalent.
The generated hook names follow the operationId pattern: GET /tasks with
operationId: listTasks becomes useListTasks().
// Before:const { data } = useQuery({ client, path: '/tasks', method: 'GET' })
// After:const { data } = useListTasks()useQuery, useMutation, QueryClient,
HydrationBoundary, and everything else you already know still work exactly as before.fetch is still the transport. No axios, no wrapper libraries.strict: true.tsc --strict).client.GET('/tasks/{id}', ...) becomes
getTask(id). Operations are named, not addressed by path.configureClient sets a module-level config. The
generated functions read it per call. No need to thread a client through your component tree.input_schema set, every request and response is
validated. You see ZodError at the boundary, not a type error three levels down.schemas.ts is yours. The generator writes it once. Add error messages, custom refinements,
and cross-field rules. They survive every regeneration.Quickstart
Full install-to-first-request walkthrough for greenfield projects.
Types and fetch client
Full reference: config options, generated file details, SSR server client, and advanced usage.
React Query hooks
Generated useQuery and useMutation hooks, key factories, suspense variants, and
queryOptions for Next.js App Router prefetching.
Full-stack tutorial
Petstore-fastify walkthrough: spec to typed client, hooks, Fastify router, and a cross-field validation error that round-trips onto the right form field.