R

@robonen/fetch

A lightweight, type-safe fetch wrapper with interceptors, retry, timeout, and a composable plugin system — V8-optimized internals, zero runtime dependencies beyond the standard library.

globalThis.fetch is great primitive plumbing, but every app re-implements the same layer on top of it: JSON parsing, throwing on 4xx/5xx, base URLs, query strings, retries, timeouts, auth headers. @robonen/fetch is that layer — small, fully typed, and built so attaching features costs nothing on the hot path.

Type-safe end to end

Response data, request options, and plugin-contributed fields are all inferred — the parsed body comes back typed, no casting required.

Smart bodies & parsing

Plain objects are JSON-serialized; FormData/Blob/streams pass through untouched. Responses are decoded from Content-Type or forced via responseType.

Retry, timeout & errors

Built-in retry and per-attempt timeout with sensible defaults, and non-2xx responses reject with a rich FetchError carrying status, request, and parsed body.

Hooks & plugins

Lifecycle hooks plus a typed, composable plugin system with onion-style execute middleware — composed once, with zero per-request overhead beyond the hooks themselves.

Install

sh
pnpm add @robonen/fetch

Quick start

Import the default $fetch instance — it is backed by globalThis.fetch and ready to use. The first type parameter types the parsed body.

ts
import { $fetch } from '@robonen/fetch';

interface Todo {
  id: number;
  title: string;
  done: boolean;
}

// GET + automatic JSON parse — the body is typed for you
const todo = await $fetch<Todo>('https://api.example.com/todos/1');

// POST a plain object — serialized to JSON, content-type set automatically
const created = await $fetch<Todo>('https://api.example.com/todos', {
  method: 'POST',
  body: { title: 'Ship it', done: false },
});

// Method shortcuts
await $fetch.get<Todo>('https://api.example.com/todos/1');
await $fetch.delete('https://api.example.com/todos/1');

Configured instances

Use create (or its alias extend) to derive instances with a baseURL, default headers, retry policy, and plugins. Configuration is merged down the chain; the child wins on conflicts.

ts
import { $fetch } from '@robonen/fetch';

// Derive a pre-configured instance — defaults & plugins are inherited
const api = $fetch.create({
  baseURL: 'https://api.example.com/v1',
  headers: { 'x-app': 'web' },
  retry: 2,
});

await api('/users');                       // → /v1/users, retry:2, x-app header
await api('/search', { query: { q: 'vue', page: 2 } });

// 'extend' layers on more defaults / plugins; child wins on conflicts
const billing = api.extend({ baseURL: 'https://billing.example.com' });

Where to next

The full API reference is listed below. A few good places to start:

  • createFetch — build a fully configured instance with defaults and plugins.
  • definePlugin — bundle defaults, typed options, hooks, and execute middleware into a reusable plugin.
  • FetchError — the rich error thrown on non-2xx responses.
  • buildURL and detectResponseType — the URL and response-type helpers used internally, exported for reuse.

API Reference

General · 14

fn
buildURL

Appends serialised query parameters to a URL string Null and undefined values are omitted. Existing query strings are preserved.

fn
callHooks

Invokes one or more lifecycle hooks with the given context

I
ComposedPlugins

Flattened hook lists and merged defaults produced by composePlugins.

fn
composePlugins

Flattens plugin defaults and hook arrays into a single shape suitable for long-lived storage on a fetch instance. Runs exactly once per createFetch call. Ordering: plugin defaults (in declaration order) → user defaults (user wins). Headers are merged independently through a single Headers instance.

fn
createFetch

Creates a configured $fetch instance

fn
createFetchError

Builds a FetchError from a FetchContext, extracting URL, status, and error message

fn
definePlugin

Declares a typed fetch plugin. Identity function — returns its input verbatim at runtime, used only to narrow generics for strong option inference.

fn
detectResponseType

Infers the response body parsing strategy from a Content-Type header value

C
FetchError

Error thrown by $fetch on network failures or non-2xx responses

fn
isJSONSerializable

Returns true when a value can be serialised with JSON.stringify

fn
isPayloadMethod

Returns true for HTTP methods that carry a request body

fn
joinURL

Joins a base URL with a relative path, normalising the slash boundary

fn
resolveFetchOptions

Merges per-request options with global defaults

fn
runHookPhase

Runs all instance-level (plugin) hooks for a single phase, then the optional user per-request hook(s). Avoids allocating an intermediate array per call.

Plugins · 2