openapi-fetch
openapi-fetch is a small Fetch API client whose TypeScript types come from an OpenAPI schema. After openapi-typescript generates a `paths` type, calls such as `client.GET('/users/{id}')` infer the allowed path, path and query parameters, body, success data, and documented error data. Runtime code stays close to fetch and works with browser, server, and framework-provided fetch implementations. It generates types, not endpoint-specific runtime methods or validators.
One of the best lightweight clients for teams that already treat OpenAPI as tested source code. If the schema is aspirational or you need runtime validation, the attractive types can hide rather than remove integration risk.
Use it if
- Your API has an accurate OpenAPI 3 schema and you want request and response types without a large generated SDK
- You want endpoint paths, parameters, bodies, success data, and documented errors inferred from one generic client
- You need a thin fetch-based runtime that works in React, Vue, Svelte, Node, or framework server loaders
- You want middleware for auth, telemetry, caching, response policy, or fetch error translation without leaving native Request and Response types
- Your OpenAPI document is missing, stale, or loosely typed: the client faithfully turns schema mistakes into misleading compile-time confidence
- You expect runtime response validation: generated TypeScript declarations disappear at runtime, and openapi-fetch parses data without checking it against the schema
- You want a zero-step install: the README requires openapi-typescript and TypeScript, a generation command, and real `tsc --noEmit` checking in CI
- You want HTTP errors to throw automatically: 4xx and 5xx responses are returned as typed `error` data unless you add response middleware that throws
- You need a mature frozen API: the current package is 0.17.0, and middleware, serializers, path-based clients, and result typing can still change before 1.0
Setup reality
Install `openapi-fetch` at runtime and `openapi-typescript` plus `typescript` as development tools. Then generate declarations, for example `npx openapi-typescript ./openapi.yaml -o ./src/api/schema.d.ts`, import the generated `paths` type, and create the client with it. That generation step belongs in a repeatable script and CI should run `tsc --noEmit`; a transpiler or linter alone may never report contract drift. The project strongly recommends `noUncheckedIndexedAccess`, which can expose unrelated unchecked lookups when enabled in an established codebase. The schema is now a build input, so decide whether to commit generated declarations or regenerate from a pinned artifact, and fail CI when output changes unexpectedly. Remote schemas may need credentials and make builds depend on network availability; a checked-in schema snapshot is more reproducible. At runtime the package expects a Fetch API implementation, defaulting to `globalThis.fetch`; framework loaders should pass their provided fetch when cookies, interception, or request context matters. Configure a `baseUrl`, but remember browser CORS and server absolute-URL rules still apply. HTTP failures do not throw by default: destructure `data`, `error`, and `response`, or register middleware that enforces throwing. Middleware order matters, with request hooks in registration order and response hooks in reverse. Request and Response bodies are stateful; middleware that reads them must clone first. Multipart bodies need a custom `bodySerializer`, and the browser must set the boundary, so do not force the JSON content type. Query arrays and objects use documented OpenAPI serialization defaults that may not match a legacy backend. Most importantly, types are only as current as the schema and provide no runtime validation of an unexpected server response.
Patterns
Generate the paths type from a local schemagenerate-types
npx openapi-typescript ./openapi.yaml -o ./src/api/schema.d.ts
npx tsc --noEmitPin the schema source and run the TypeScript compiler in CI. Generated types do not update until this command runs again.
Create a typed clientcreate-client
import createClient from 'openapi-fetch';
import type {paths} from './schema';
export const client = createClient<paths>({
baseUrl: 'https://api.example.com/v1/',
});The paths import must come from the generated declaration. Keep the base URL environment-specific without changing the schema path keys.
GET with typed path and query parametersget-path-params
const {data, error, response} = await client.GET('/users/{user_id}', {
params: {
path: {user_id: '42'},
query: {include: 'teams'},
},
});
if (error) console.error(response.status, error);
else console.log(data);The literal path must match the schema key exactly. Success and error shapes are inferred from the documented responses.
POST a schema-checked bodypost-json
const result = await client.POST('/users', {
body: {name: 'Ada', email: 'ada@example.com'},
});
if (result.error) throw new Error('create failed');
console.log(result.data.id);The body is checked at compile time and serialized as JSON by default. It is not validated again at runtime.
Make non-2xx responses throwthrow-http-errors
import type {Middleware} from 'openapi-fetch';
const throwOnError: Middleware = {
onResponse({response}) {
if (!response.ok) {
throw new Error(`${response.url}: ${response.status} ${response.statusText}`);
}
},
};
client.use(throwOnError);onError handles exceptions from fetch, not 4xx or 5xx responses. Response middleware is the place to impose throwing semantics.
Attach a fresh bearer tokenattach-auth
const authMiddleware: Middleware = {
onRequest({request}) {
const token = tokenStore.current();
if (token) request.headers.set('Authorization', `Bearer ${token}`);
return request;
},
};
client.use(authMiddleware);Module-level token state is unsafe for multi-user server processes. Read authentication from request-scoped context there.
Cancel a typed requestabort-request
const controller = new AbortController();
const pending = client.GET('/reports/{id}', {
params: {path: {id: 'monthly'}},
signal: controller.signal,
});
controller.abort();
await pending;An abort is a fetch exception, not typed HTTP error data. Handle DOMException or translate it in onError middleware.
Serialize a multipart requestupload-form-data
await client.POST('/uploads', {
body: {file, caption: 'invoice'},
bodySerializer(body) {
const form = new FormData();
form.append('file', body.file);
form.append('caption', body.caption);
return form;
},
});When the serializer returns FormData, openapi-fetch omits Content-Type so the browser can add the multipart boundary.
Match an API's array query formatcustomize-query-arrays
const client = createClient<paths>({
baseUrl: 'https://api.example.com/',
querySerializer: {
array: {style: 'pipeDelimited', explode: false},
object: {style: 'deepObject', explode: true},
},
});The default for arrays is exploded form style. Set this only when the OpenAPI server contract expects another serialization.
Read a response as a streamstream-response
const {data: stream, error} = await client.GET('/exports/{id}', {
params: {path: {id: 'latest'}},
parseAs: 'stream',
});
if (error) throw new Error('export failed');
const reader = stream.getReader();parseAs stream skips response parsing. The generated success type may need schema metadata that accurately describes the binary response.
Pass a framework-scoped fetch implementationuse-framework-fetch
export function apiFor(fetch: typeof globalThis.fetch) {
return createClient<paths>({
baseUrl: 'https://api.example.com/',
fetch,
});
}Framework fetch wrappers can carry cookies, caching, tracing, or test interception that globalThis.fetch would bypass.
Inspect the generated Request in a unit testtest-request
const mockFetch = vi.fn(async () => Response.json({id: '42'}));
const testClient = createClient<paths>({
baseUrl: 'https://api.example.com/',
fetch: mockFetch,
});
await testClient.POST('/users', {body: {name: 'Ada'}});
const request = mockFetch.mock.calls[0][0] as Request;
expect(await request.json()).toEqual({name: 'Ada'});A Request body is single-use. Clone it first if middleware or multiple assertions also need to consume it.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| openapi-client-axios | npm | You want OpenAPI operation lookup on top of Axios and prefer its interceptors and response model |
| openapi-typescript-fetch | npm | You prefer generated operation functions and a smaller older fetch-oriented API |
| axios | npm | Your API lacks a trustworthy OpenAPI schema and manual request types plus mature interceptors are a better fit |