Skip to content

Migrating from openapi-fetch (openapi-typescript)

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:

  1. Maintenance mode. Discussion #2559 makes it explicit: the package is winding down. You are on your own for new OpenAPI 3.1 constructs and future React Query v5 compatibility.
  2. No runtime validation. 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.
  3. Zod v4 as a free upgrade. The 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.
  4. Server generation is missing from the ecosystem. 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
},
})
// openapi-fetch style: path string, generic runtime object
const { 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 error
console.log(data.title)
const { data, error } = await client.POST('/tasks', {
body: { title: 'New task', status: 'open' },
})
if (error) throw error
// 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',
})
// ...
}
import { useMutation } from '@openapi-ts/react-query'
function CreateTaskButton() {
const { mutate } = useMutation({
client,
path: '/tasks',
method: 'POST',
})
// ...
}

Zod v4 runtime validation: the free upgrade

Section titled “Zod v4 runtime validation: the free upgrade”

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(),
})
  1. Remove openapi-fetch and openapi-typescript.

    Terminal window
    npm uninstall openapi-fetch openapi-typescript @openapi-ts/react-query

    Delete any generated openapi-types.ts and paths.d.ts files.

  2. Install the new generator.

    Terminal window
    npm install -D openapi-zod-ts

    For React Query hooks:

    Terminal window
    npm install -D @codewithagents/openapi-react-query
    npm install @tanstack/react-query # already installed if you used openapi-react-query
  3. Create 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.

  4. Run the generators.

    Terminal window
    npx openapi-zod-ts
    npx openapi-react-query

    Or add to package.json:

    {
    "scripts": {
    "generate": "openapi-zod-ts && openapi-react-query"
    }
    }
  5. 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() })
  6. 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)
  7. 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()
  • React Query v5 is still the engine. useQuery, useMutation, QueryClient, HydrationBoundary, and everything else you already know still work exactly as before.
  • Native fetch is still the transport. No axios, no wrapper libraries.
  • TypeScript strict mode is required by both. Generated output passes strict: true.
  • OpenAPI 3.0.x and 3.1.x are both supported. 3.1 is the primary target; 3.0.x works in practice (8 of 13 showcase specs are 3.0.x and all compile under tsc --strict).
  • Generated functions instead of path strings. client.GET('/tasks/{id}', ...) becomes getTask(id). Operations are named, not addressed by path.
  • No client singleton to pass around. configureClient sets a module-level config. The generated functions read it per call. No need to thread a client through your component tree.
  • Runtime validation is explicit. With 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.

Types and fetch client

Full reference: config options, generated file details, SSR server client, and advanced usage.

openapi-zod-ts guide

React Query hooks

Generated useQuery and useMutation hooks, key factories, suspense variants, and queryOptions for Next.js App Router prefetching.

openapi-react-query guide

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.

Full-stack tutorial