Version v2.2.0 Size 4.9 KB gzip
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 }),
});| Feature | Courier | TanStack Query | ky |
|---|---|---|---|
| Bundle size | 4.9 KB | Framework adapter required | Separate package |
| Zero runtime dependencies | |||
| Native fetch transport | Bring 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/couriersh
npm install @vielzeug/couriersh
yarn add @vielzeug/courierQuick 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 withinvalidateKeysfor 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.