mrkeyoor.com_
Tue 22 Sept 18:49 UTC
npmWeb Frontendupdated 22 Sept 2026

openapi-fetch review

openapi-fetch wraps the native Fetch API with TypeScript types generated from an OpenAPI document. Give `createClient` a generated `paths` type, then `GET('/users/{id}')` knows the allowed URL templates, parameters, body, success payload, and documented error payload. Runtime code still builds a normal `Request` and returns a normal `Response` alongside parsed `data` or `error`. It does not generate endpoint classes or validate server data at runtime. Version 0.17.0 adds read-only and write-only markers shared with openapi-typescript, so generated response and request shapes can exclude the wrong side of those fields. It also stops treating `Content-Length: 0` as empty when a response uses chunked transfer encoding.

Verdict

openapi-fetch gives schema-disciplined TypeScript teams a 2.9 KB gzipped client with little runtime ceremony. Do not install it to compensate for an unreliable schema or when runtime validation and a post-1.0 contract are required.

We installed it

Lab card: what happened when we installed openapi-fetchScreenshot of openapi-fetch documentation
Install✓ · 0.6s2 packages on disk · 1 MB
ImportESM import works · require() works · ESM package with exports map
Browser2.9 KBgzipped (7.4 KB minified), bundled with esbuild
Typesno TypeScript types found
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does openapi-fetch install cleanly?

Yes. In a fresh container with an empty cache, npm install openapi-fetch finished in 0.6s, leaving 2 packages and 1 MB on disk. npm audit reported no known vulnerabilities.

How much does openapi-fetch add to a browser bundle?

2.9 KB gzipped (7.4 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.

Does openapi-fetch work with both ESM and CommonJS?

Yes. Both import 'openapi-fetch' and require('openapi-fetch') worked in Node 22 in our run. The package is published as ESM with an exports map.

Does openapi-fetch include TypeScript types?

No type declarations were found in our install, so TypeScript users need their own declarations.

openapi-fetch or openapi-client-axios: which should you use?

openapi-client-axios: Choose it when operation lookup should sit on Axios and use its interceptors and response model. openapi-fetch gives schema-disciplined TypeScript teams a 2.9 KB gzipped client with little runtime ceremony.

When should you not use openapi-fetch?

The schema is stale or aspirational. openapi-fetch will turn an incorrect contract into convincing editor completions and compile-time checks.

API stability3/5The method shape is compact and familiar: create a client, call an HTTP verb with a schema path, pass typed params or a body, and inspect `data`, `error`, and `response`. Middleware works with native Request and Response. The package remains at 0.17.0, and recent releases have touched path clients, serializers, middleware behavior, and type helpers. The new read/write markers also require coordinated generator settings, so lock related package versions and treat minor upgrades as review work.
Docs5/5The official site covers client and per-request options, path-based clients, query, path, and body serializers, every response parsing mode, middleware return values, execution order, ejection, authentication, caching, testing with injected Fetch and MSW, and schema generation. It calls out one-use response bodies and distinguishes network exceptions from 4xx and 5xx responses. The docs also insist on `tsc --noEmit`, which addresses a common gap in TypeScript build pipelines.
Maintenance4/5Version 0.17.0 shipped on February 11, 2026, and the monorepo was pushed on August 23. GitHub reports 8,325 stars, 284 open issues and pull requests, an unarchived repository, and active work on main. The release coordinates openapi-fetch with openapi-typescript helpers, fixes chunked zero-length handling, and adds read/write property semantics. Monorepo activity is healthy, though six months without a client release and the pre-1.0 version warrant pinned upgrades.
Ecosystem4/5npm counted 7,561,348 downloads in the latest completed week. The client works wherever Fetch exists, accepts an injected implementation for frameworks and tests, and shares generated schemas with openapi-typescript and openapi-react-query. Our install used two packages and the browser build was 2.9 KB gzipped. The tradeoff is a coupled toolchain: schema source, generator version, helper types, compiler settings, and runtime client all have to move together.

Use it if

  • Your OpenAPI 3 document is reviewed with the service and is accurate enough to be a TypeScript build input.
  • A typed client should stay close to `fetch` instead of generating one runtime function or class per operation.
  • Browser, Node, and framework loaders need the same path and payload inference with swappable Fetch implementations.
  • Authentication, logging, caching, or response policy can be expressed as middleware over native Request and Response objects.
Skip it if

Setup reality

We installed openapi-fetch 0.17.0 in a clean Node 22 Bookworm container. npm finished in 0.6 seconds and left two packages using 1 MB. The package has one direct dependency and no peers, is 272 KB unpacked, and uses MIT. npm audit found no known vulnerabilities. It is ESM with an exports map, though both CommonJS require() and ESM import worked in our checks. Our package scan found no TypeScript types. The measured browser bundle was 7.4 KB minified and 2.9 KB gzipped.

The client becomes useful after a separate generation step. Install openapi-typescript and TypeScript as development tools, then generate a paths declaration from a checked-in or pinned schema. Put that command in package.json, decide whether generated output is committed, and have CI run tsc --noEmit; a transpiler may never report schema drift. Version 0.17.0's read/write separation needs openapi-typescript --read-write-markers and the matching helper types. Without the flag, those schema markers do not change request and response shapes.

At runtime, set a baseUrl and pass the request context's Fetch implementation when a framework supplies one. That preserves cookies, tracing, mocks, or server-specific behavior. Calls resolve with data, error, and response; a 404 is an HTTP response, so onError middleware does not see it. Add onResponse policy if callers should receive thrown errors. Request middleware runs in registration order and response middleware runs in reverse. Clone a response before reading its body in middleware because Fetch bodies can be consumed once.

The default body serializer uses JSON. Multipart requests need a bodySerializer that returns FormData; let the runtime add the boundary instead of forcing Content-Type. Query arrays default to form style with explode enabled, while objects use deepObject with explode enabled. Legacy backends often disagree, so configure querySerializer and cover exact URLs in tests. Generated types offer no runtime guard against a server that violates the schema, so validate at trust boundaries when bad payloads could corrupt state.

Patterns

Generate the paths declaration generate-schema-types

npx openapi-typescript ./openapi.yaml -o ./src/api/schema.d.ts

Pin the schema source and generator version. Run `tsc --noEmit` after generation to catch contract drift.

Create a typed Fetch client create-api-client

import createClient from 'openapi-fetch'
import type { paths } from './schema'

export const api = createClient<paths>({
  baseUrl: 'https://api.example.com/v1',
})

The generic comes from openapi-typescript output. `baseUrl` still follows normal URL and browser CORS rules.

Fetch one typed resource get-with-path-param

const { data, error, response } = await api.GET('/users/{user_id}', {
  params: { path: { user_id: 'u_42' } },
})
if (error) throw new Error(`request failed: ${response.status}`)
console.log(data.email)

The schema controls the path key, parameter name, and data and error shapes.

Post a schema-checked body send-json-body

const result = await api.POST('/orders', {
  body: { sku: 'TONER-42', quantity: 2 },
})
if (result.error) return { ok: false, issue: result.error }
return { ok: true, order: result.data }

The default body serializer uses JSON. Required fields and documented errors come from the schema.

Read the current token per request attach-auth-middleware

import type { Middleware } from 'openapi-fetch'

const auth: Middleware = {
  onRequest({ request }) {
    const token = readAccessToken()
    if (token) request.headers.set('Authorization', `Bearer ${token}`)
    return request
  },
}
api.use(auth)

Resolve the token inside the hook so refreshes are visible. Avoid shared module token state in multi-user servers.

Enforce a throwing response policy throw-on-http-error

const requireOk = {
  onResponse({ response }: { response: Response }) {
    if (!response.ok) throw new Error(`HTTP ${response.status}`)
    return response
  },
}
api.use(requireOk)

`onError` handles Fetch exceptions, not 4xx or 5xx responses. Use `onResponse` for HTTP status policy.

Translate Fetch failures handle-network-error

api.use({
  onError({ error }) {
    return new Error('API transport failed', { cause: error })
  },
})

Network failures, CORS errors, and aborts can reach this hook. Preserve the original cause for diagnosis.

Choose query array syntax serialize-query-arrays

const api = createClient<paths>({
  baseUrl: 'https://api.example.com',
  querySerializer: {
    array: { style: 'form', explode: false },
    object: { style: 'deepObject', explode: true },
  },
})

With `explode: false`, an array becomes a comma-separated value. Match the OpenAPI parameter definition and real server parser.

Serialize a multipart body upload-multipart

await api.POST('/documents', {
  body: { file, title: 'Invoice' },
  bodySerializer(body) {
    const form = new FormData()
    form.set('file', body.file)
    form.set('title', body.title)
    return form
  },
})

Do not set the multipart Content-Type header manually; Fetch adds the boundary for FormData.

Request text instead of JSON parse-text-response

const { data, error } = await api.GET('/reports/{id}/text', {
  params: { path: { id: 'r_7' } },
  parseAs: 'text',
})

Other built-in modes include arrayBuffer, blob, and stream. The schema and chosen parser still need to agree.

Keep request context in a server loader inject-framework-fetch

export function makeApi(fetchForRequest: typeof fetch) {
  return createClient<paths>({
    baseUrl: 'https://api.example.com',
    fetch: fetchForRequest,
  })
}

Use the framework-provided Fetch when it carries cookies, tracing, mocks, or platform behavior.

Inspect a generated Request unit-test-request

const mockFetch = vi.fn().mockResolvedValue(
  new Response(JSON.stringify({ id: 'u_1' }), {
    status: 200,
    headers: { 'Content-Type': 'application/json' },
  }),
)
const client = createClient<paths>({ baseUrl: 'https://api.test', fetch: mockFetch })
await client.GET('/users/{user_id}', { params: { path: { user_id: 'u_1' } } })
expect(mockFetch.mock.calls[0][0]).toBeInstanceOf(Request)

Injecting Fetch tests URL and request construction without a live service. Use MSW when response behavior matters across more of the app.

Alternatives

PackageRegistryPick it when
openapi-client-axiosnpmChoose it when operation lookup should sit on Axios and use its interceptors and response model.
openapi-typescript-fetchnpmChoose it for an older generator-led Fetch client with operation functions.
swagger-clientnpmChoose it when the application must load and execute an OpenAPI document at runtime rather than compile generated types.

More web frontend guides

postcss · react · react-dom · tailwindcss · htmlparser2 · tailwind-merge · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.