@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.

general · 15

V
$fetch

Default $fetch instance backed by globalThis.fetch

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

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

V
NULL_BODY_STATUSES

HTTP status codes whose responses never have a body

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