Skip to content
courier logoCourierHTTP
A framework-neutral fetch client with explicit cache keys, direct mutations, and abortable streams.
Version
v2.2.0
Size
4.9 KB gzip
BrowserNode ≥22SSRDeno
createCourierCourierErrorCourierHttpErrorCourierNetworkErrorCourierTimeoutError View all 10 exports

Why Courier?

Native fetch leaves request policy, cached reads, and stream lifecycles to each application. Courier keeps those concerns in one client while making cache identity and fetch policy explicit at every cached read.

ts
// Before
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const user = await response.json();

// After
await courier.queries.fetch({
  key: ['users', userId],
  fetch: ({ signal }) => courier.get('/users/{id}', { params: { id: userId }, signal }),
});
FeatureCourierTanStack Queryky
Bundle size4.9 KBFramework adapter requiredSeparate package
Zero runtime dependencies
Native fetch transportBring your own
Explicit cache keys
SSE and NDJSON iteration
External runtime dependencies

Use Courier when one application client should own typed HTTP, explicit cached reads, direct writes, and abortable response streams.

Consider TanStack Query when you need a maintained framework adapter or advanced cache features such as infinite queries. Consider ky when you only need a compact fetch wrapper without caching or streams.

Installation

sh
pnpm add @vielzeug/courier
sh
npm install @vielzeug/courier
sh
yarn add @vielzeug/courier

Quick Start

Create one client for an application or request scope, then fetch a cache entry by its explicit key.

ts
import { CourierHttpError, createCourier } from '@vielzeug/courier';

type User = { id: number; name: string };

const courier = createCourier({ baseUrl: 'https://api.example.com', query: { staleTime: 30_000 } });
const key = ['users', 42] as const;

try {
  await courier.queries.fetch({
    key,
    fetch: ({ signal }) => courier.get('/users/{id}', { params: { id: 42 }, signal }),
  });
  console.log(courier.queries.getSnapshot<User>(key)?.data);
} catch (error) {
  if (CourierHttpError.is(error, 404)) console.log('User not found');
  else throw error;
} finally {
  courier.dispose();
}

Features

  • createCourier() — one lifecycle, interceptor pipeline, header store, and cancellation boundary.
  • get() / post() / put() / patch() / delete() — typed paths, query strings, request bodies, validation, and structured errors.
  • queries.fetch() — key-based cached reads, subscriptions, invalidation with refetch, and automatic garbage collection.
  • mutate() — direct write operation with invalidateKeys for one-step cache refetch, without hidden retries or a second state store.
  • events() / read() — abortable SSE, text, and NDJSON iteration with normalized request errors.
  • withBearerAuth() / withRequestId() / withLogging() — composable transport policies.

Documentation

See Also

  • Flux — adapts Courier cache entries and event iterators into composable streams.
  • Ripple — stores Courier snapshots in fine-grained reactive state.
  • Spell — validates parsed HTTP payloads through Courier's schema option.