# ofetch: a typed fetch wrapper for Node, browsers and workers

> ofetch adds automatic JSON parsing, typed responses, retries and lifecycle interceptors on top of the native fetch API. It is a thin convenience layer, not a replacement runtime, and the v2 branch is still in alpha.

**unjs/ofetch** — 😱 A better fetch API. Works everywhere.

- Repository: https://github.com/unjs/ofetch
- Stars: 5,362 · Forks: 198
- Language: TypeScript
- License: MIT
- Published: 2026-09-22 · Updated: 2026-09-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/unjs-ofetch

## What ofetch adds on top of the native fetch API

The native fetch API returns a Response object. You call response.json(), check response.ok yourself, and write the same boilerplate in every file. ofetch is a wrapper that does that work for you and keeps the same call shape, so a request that would be `fetch(url).then(r => r.json())` becomes `await ofetch(url)` and returns the parsed body directly. The README states that for binary content types ofetch returns a Blob instead of parsed JSON.

The target audience is JavaScript and TypeScript developers who already chose fetch over an HTTP client library and now want the ergonomics back. It works on Node, in the browser and in workers, which matters if the same request code runs in a server route and in a client component. Because it is built on fetch, it inherits the platform's request and response types rather than introducing its own.

## Response parsing, JSON bodies and type assistance

The parsing rule is the part that changes daily code the most. ofetch parses JSON responses automatically, and you can override the parser with `parseResponse`, or force a specific body method with `responseType` set to `blob`, `arrayBuffer`, `text` or `stream`. That last option is how you get a stream instead of a buffered body.

On the request side, passing an object or a class with a `toJSON()` method to `body` makes ofetch stringify it with `JSON.stringify()`. Classes without `toJSON()` must be converted to a string first, which is a real constraint if you pass model instances around. For `PUT`, `PATCH` and `POST` with a string or object body, ofetch sets `content-type: application/json` and `accept: application/json` by default; both can be overridden.

Type assistance is a generic parameter, not runtime validation:

```ts
const article = await ofetch<Article>(`/api/article/${id}`);
```

The README notes autocomplete then works on `article.id`. Nothing checks that the server actually returned that shape.

## Installing ofetch and making a first request

The README's quick start uses nypm to install the package. Any package manager works; the repository itself pins `pnpm@10.20.0` in package.json.

```bash
npx nypm i ofetch
```

Then import the named export. The package is ESM only: package.json sets `"type": "module"` and the `exports` field points at `./dist/index.mjs` with types at `./dist/index.d.mts`.

```js
import { ofetch } from "ofetch";

const { users } = await ofetch("/api/users");
```

The call resolves to the parsed body, so `users` is already an object. If the server returns a non-2xx status, ofetch throws instead of resolving, so wrap the call or attach a catch. The repository ships an `examples/` directory with `first-request.mjs`, `body.mjs`, `headers.mjs`, `methods.mjs`, `error-handling.mjs`, `query-string.mjs`, `proxy.mjs` and `type-safety.ts` if you want runnable starting points.

## Error handling, retries and the POST caveat

When `response.ok` is false, ofetch throws a `FetchError` with a compact stack, and the parsed error body is available on `error.data`. The README's example output is `FetchError: [GET] "https://google/404": 404 Not Found`. To bypass throwing entirely, set `ignoreResponseError: true`.

Retries are automatic only for a fixed list of status codes: 408, 409, 425, 429, 500, 502, 503 and 504. You can change the count with `retry`, the gap with `retryDelay` (default 0 ms) and the set of codes with `retryStatusCodes`.

```ts
await ofetch("http://google.com/404", {
  retry: 3,
  retryDelay: 500, // ms
  retryStatusCodes: [404, 500], // response status codes to retry
});
```

The default is one retry, but the README states ofetch does not retry `POST`, `PUT`, `PATCH` or `DELETE` by default to avoid side effects. Setting `retry` to any custom value makes it retry for all methods, including those. That is the sharpest edge in the library: raising the retry count to survive flaky GETs silently changes write behaviour too. If your writes are not idempotent, you have to gate retries yourself.

Timeouts are opt-in. The `timeout` option takes milliseconds and aborts the request; the README says the default is disabled, so a request without it can hang indefinitely.

## Interceptors, baseURL and shared defaults with ofetch.create

Four lifecycle hooks are available per call: `onRequest`, `onRequestError`, `onResponse` and `onResponseError`. `onRequest` fires as soon as ofetch is called and receives `{ request, options }`, so it can mutate options, which is how the README's example appends a timestamp to the query. `onResponse` fires after the fetch call and after body parsing; `onResponseError` fires when the response arrives but `response.ok` is not true. Each hook can also be an array of functions called sequentially.

The pattern for shared behaviour is `ofetch.create`, which returns a new instance with default options. The README notes that defaults are cloned one level and inherited, so nested option objects are shared rather than deep-copied. That is worth knowing before mutating a nested object inside an interceptor on a created instance.

URL building is delegated to ufo. `baseURL` prepends the base while handling leading and trailing slashes, and the `query` option (aliased as `params`) merges search params into the URL while preserving any query already present in the request string. So `ofetch("/movie?lang=en", { query: { id: 123 } })` keeps `lang=en` and adds `id`.

## Where ofetch is the wrong tool

ofetch does nothing about request cancellation beyond the `timeout` option, and the README does not document an AbortSignal option of its own; you are relying on the underlying fetch options being passed through. If your application needs to cancel in-flight requests on navigation, that is a design question you answer outside ofetch.

The bigger boundary is write-heavy APIs. Because custom `retry` values apply to every method, an application that wants aggressive retry on reads and none on writes cannot express that with a single option. You either leave `retry` at its default and accept one retry on reads, or you set a value and take responsibility for idempotency yourself.

Finally, the README is explicit that the main branch is v2 alpha development and that v1 documentation lives on the `v1` branch. The latest release listed is v1.5.1, while the repository's package.json carries version `2.0.0-alpha.3`. If you install from the default npm tag you get v1; the alpha is a separate line with its own release tag. Teams that cannot absorb alpha churn should pin v1 and watch for the v2 release notes.

## ofetch compared with Axios and Ky

Axios predates fetch. It ships its own transport, which historically meant an XHR-based client in browsers, and it has its own interceptor API, its own error object shape and its own cancellation token. It also works in environments where fetch is missing, and it has a documented fetch adapter for cases where you want Axios' interface on top of fetch. Choosing Axios means adopting a request stack; choosing ofetch means keeping the platform's.

Ky is the closer comparison. It is also a fetch wrapper, and the searches around it mention a timeout error, so timeout behaviour is a visible part of its interface. The practical difference for a reader deciding between them is surface area and ecosystem: ofetch belongs to the UnJS set of packages, so its `baseURL` and `query` handling come from ufo and it composes with the rest of that toolkit, while Ky is a standalone client with its own hook names. Neither is a superset of the other.

If your code already calls fetch and the only pain is repeated parsing and error checks, ofetch is the smaller step. If you need an HTTP client that abstracts the transport itself, ofetch is not that.

## Licence, maintenance and upgrade cost

ofetch is MIT licensed, and the repository contains a LICENSE file at the top level. MIT is permissive: it allows commercial use and modification with the copyright notice retained. This is a description of the licence text, not legal advice; check the LICENSE file in the version you install.

The last push to the repository was on 2026-09-21, and the repository is not archived. The most recent release listed is v1.5.1 from 2025-11-01, followed by two v2.0.0 alpha releases in October 2025. So the stable line and the development line are visibly separate, and the gap between the last stable release and today is substantial even though the repository itself is being pushed to.

The upgrade cost sits in that split. Moving from v1 to v2 means reading the v2 branch documentation rather than the v1 docs, since the README states plainly that it documents v2 alpha. The release script in package.json publishes with `--publishTag alpha`, which is how the alpha versions avoid becoming the default install. If you depend on ofetch, pin an explicit version range rather than trusting a floating tag, and read CHANGELOG.md before moving across the v1/v2 boundary.

## Conclusion

Adopt ofetch when your codebase already targets native fetch and you want parsing, retries and shared interceptors without pulling in an HTTP client with its own adapter layer. Skip it if you need automatic retry on POST or DELETE, since the README states ofetch does not retry those methods by default, or if you depend on Axios-specific features such as its interceptor API and request cancellation semantics. Before adopting, check whether the version you install is v1 or the v2 alpha, because the README marks the main branch as v2 development and points v1 users to a separate branch.

## FAQ

### What is the Fetch API used for?

The native fetch API is the platform's HTTP request primitive, and ofetch is built on top of it. ofetch keeps the same call shape while adding automatic JSON parsing, typed responses, retries and interceptors.

### Which is better, Ajax or Fetch?

The README does not compare Ajax with fetch. It only positions ofetch against other fetch wrappers such as Axios and Ky, noting that Axios ships its own transport rather than relying on the platform's fetch.

### Why do people use Axios instead of fetch?

Axios ships its own transport, its own interceptor API, its own error object shape and its own cancellation token, and it works where fetch is missing. It also offers a documented fetch adapter for running its interface on top of fetch.

### What are the disadvantages of the Fetch API?

The README does not list fetch's disadvantages directly, but the features ofetch adds point at them: you parse JSON yourself, check response.ok yourself, and write retry and timeout logic yourself. ofetch also notes that binary content types return a Blob rather than parsed JSON.

### What are the alternatives to ofetch?

Axios and Ky are the two alternatives discussed here. Axios abstracts the transport itself, while Ky is another fetch wrapper with its own hook names and a timeout interface; ofetch differs by delegating URL handling to ufo and composing with the UnJS package set.

## Sources

- [Issues](https://github.com/unjs/ofetch/issues)
- [License: MIT](https://github.com/unjs/ofetch/blob/main/LICENSE)
- [README](https://github.com/unjs/ofetch/blob/main/README.md)
- [Releases](https://github.com/unjs/ofetch/releases)
- [unjs/ofetch on GitHub](https://github.com/unjs/ofetch)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/unjs-ofetch
