Version v3.0.0 Size 5.4 KB gzip
Why Courier?
Native fetch leaves request policy, error classification, middleware composition, and repeated-read coordination to each application. Courier keeps those concerns in one client, including explicit cached GETs and warm-cache prefetching, while observable query and mutation state remain consumer-owned.
// Before
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const user = await response.json();
// After
const user = await courier.get<User>('/users/{id}', { params: { id: userId } });| Feature | Courier | ky | ofetch |
|---|---|---|---|
| Bundle size | 5.4 KB | Separate package | Separate package |
| Zero runtime dependencies | |||
| Native fetch transport | |||
| Explicit parsed-value cache and prefetch | |||
| Immutable middleware at construction | |||
| Structured error taxonomy |
Use Courier when one client should own typed HTTP, explicit cached GETs and prefetching, immutable middleware, and structured errors — while a separate state layer owns observable queries and mutations.
Consider ky when you only need a compact fetch wrapper. Consider ofetch when you want a bundled fetch utility with its own retry and caching conventions.
Installation
pnpm add @vielzeug/couriernpm install @vielzeug/courieryarn add @vielzeug/courierQuick Start
Create one transport client for an application or request scope, then use method conveniences for standard HTTP methods and request() for custom methods.
import { CourierHttpError, createCourier } from '@vielzeug/courier';
type User = { id: number; name: string };
const courier = createCourier({ baseUrl: 'https://api.example.com' });
try {
const user = await courier.get<User>('/users/{id}', { params: { id: 42 } });
console.log(user.name);
} catch (error) {
if (CourierHttpError.is(error, 404)) console.log('User not found');
else throw error;
} finally {
courier.dispose();
}Features
createCourier()— one lifecycle, immutable middleware pipeline, header defaults, and cancellation boundary.request()— one contract for every HTTP method; passmethodin the config.- Cached
get()— opt in with a structured key and optional per-read TTL; successful parsed values share bounded storage and in-flight work. prefetch()— await cache warming while request failures remain observable throughtap().invalidateCache()/clearCache()— invalidate structured key prefixes without query state.get()/post()/put()/patch()/delete()— conveniences for standard methods.withBearerAuth()/withRequestId()/withLogging()— composable middleware configured at construction.tap()— signal-owned structured transport observation.- Error taxonomy — HTTP, network, timeout, abort, parse, and schema failures are distinct, actionable classes.
cancelAll()/dispose()— abort active requests and tear down the transport.
Documentation
See Also
- Flux — composes async streams; pair with Courier transport calls in the owning state layer.
- Spell — validates parsed HTTP payloads through Courier's
schemaoption. - Postmaster — coordinates durable delivery of Courier requests through a job outbox.