Skip to content

API Overview

SymbolPurposeExecution modeCommon gotcha
createCourier()Creates unified application clientSyncDispose only when whole scope ends
Courier HTTP methodsSends and parses HTTP requestsAsyncDirect calls never deduplicate
queries.fetch()Fetches one keyed cache entryAsyncKey must include all response identity inputs
mutate()Runs one write operationAsyncIt never retries automatically
events() / read()Opens abortable response iteratorsAsync iterationBreaking iteration aborts request
withBearerAuth()Adds authorization interceptorSyncToken provider runs per request
withRequestId()Adds request identifier interceptorSyncDefault generator uses uuid()
withLogging()Logs request result metadataSyncRequires explicit logger; URLs may contain sensitive query values

Package Entry Point

ImportPurpose
@vielzeug/courierClient factory, errors, interceptors, and public types

Client

createCourier()

ts
createCourier(options?: CourierOptions): Courier;

Returns client sharing transport configuration, headers, interceptors, cancellation, cache, mutations, and streams.

CourierOptions fieldTypeDefaultDescription
baseUrlstring''Prefix for relative request paths
fetchtypeof globalThis.fetchglobalThis.fetchFetch implementation
headersRecord<string, string>{}Global request headers
timeoutnumber30_000Default HTTP timeout in milliseconds
query.staleTimenumber0Cache freshness duration
query.gcTimenumber300_000Garbage-collect entries with no subscribers after this duration (ms); Infinity disables

Returns: Courier.

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

const courier = createCourier({ baseUrl: 'https://api.example.com' });
Courier memberSignatureDescription
get / post / put / patch / delete<T, P>(url: P, config?) => Promise<T>Sends one HTTP request
setHeaders(updates) => voidUpdates global headers
getHeaders() => Readonly<Record<string, string>>Returns header snapshot
use(interceptor) => () => voidRegisters interceptor
cancelAll() => voidAborts active HTTP, cache, and mutation work
queriesQueryCacheOwns keyed cache entries
mutate<T>(options) => Promise<T>Runs one write operation
events<T, P>(url, options?) => AsyncIterableIterator<StreamEvent<T>>Opens SSE iterator
read<T, P>(url, options?) => AsyncIterableIterator<T>Opens text or NDJSON iterator
dispose() => voidFinal disposal; aborts work and clears cache
disposedbooleanWhether final disposal occurred
disposalSignalAbortSignalAborts on final disposal

Queries

queries.fetch()

ts
fetch<T>(definition: QueryDefinition<T>, options?: { force?: boolean }): Promise<T>;

Registers latest definition for definition.key, then returns fresh cached data or runs its fetch function.

ParameterTypeDescription
definition.keyQueryKeyCache identity; include every response identity input
definition.fetch(context: QueryContext) => Promise<T>Request function for this key
definition.staleTimenumberPer-entry freshness duration
options.forcebooleanFetch even when cached data is fresh

Returns: Cached or fetched data.

ts
const key = ['profile', 1] as const;
await courier.queries.fetch({
  key,
  fetch: ({ signal }) => courier.get('/profile/{id}', { params: { id: 1 }, signal }),
});
QueryCache methodReturnsDescription
get(key)T | undefinedReturns successful cached data
getSnapshot(key)AsyncState<T> | nullReturns snapshot by key
set(key, data, options?)voidSets successful cache value
invalidate(prefix, options?)voidMarks matching key prefixes stale; options.refetch triggers background refetch
keys()QueryKey[]Lists known keys
subscribe(key, listener)UnsubscribeSubscribes to one key
clear()voidRemoves every cache entry

Mutations

mutate()

ts
mutate<T>(options: MutationOptions<T>): Promise<T>;

Runs options.request once, then calls onSuccess after successful completion, then invalidates (and refetches) each key in invalidateKeys.

MutationOptions<T> fieldTypeDescription
request(context: MutationContext) => Promise<T>Write operation
onSuccess(data, queries) => void | Promise<void>Cache update callback
invalidateKeysreadonly (readonly unknown[])[]Key prefixes to invalidate and refetch after success
signalAbortSignalCaller-controlled cancellation

Returns: Request result.


Streams

events() and read()

ts
events<T, P extends string>(url: P, options?: StreamOptions<P>): AsyncIterableIterator<StreamEvent<T>>;
read<T, P extends string>(url: P, options?: StreamOptions<P> & { parse?: 'ndjson' | 'text' }): AsyncIterableIterator<T>;

Both iterators abort request when return() runs or for await loop exits. events() parses event and data fields; it does not retain event IDs or reconnect.

StreamOptions<P> extends RequestConfig<P> (typed path params) with an optional method field. It omits responseType and schema (not applicable to streaming).

Returns: Abortable async iterator.


Interceptors

Interceptor helpers

ts
withBearerAuth(token: string | (() => string | Promise<string>)): Interceptor;
withRequestId(options?: { generate?: () => string; header?: string }): Interceptor;
withLogging(options: {
  logger: (message: string, meta: { duration: number; method: string; status: number; url: string }) => void;
}): Interceptor;

Each helper returns an Interceptor accepted by courier.use(). withLogging requires an explicit logger function — no default console output.

Types

ts
type AsyncState<T> =
  | { data: undefined; error: null; isFetching: boolean; status: 'loading'; updatedAt: undefined }
  | { data: T; error: null; isFetching: boolean; status: 'success'; updatedAt: number }
  | { data: T | undefined; error: Error; isFetching: false; status: 'error'; updatedAt: number };

type QueryContext = { readonly key: QueryKey; readonly signal: AbortSignal };
type QueryDefinition<T> = { fetch: (context: QueryContext) => Promise<T>; key: QueryKey; staleTime?: number };
type QueryKey = readonly [QueryKeyAtom, ...QueryKeyAtom[]];
type QueryKeyAtom = string | number | boolean | null;
type QueryCache = {
  clear(): void;
  fetch<T>(definition: QueryDefinition<T>, options?: { force?: boolean }): Promise<T>;
  get<T>(key: QueryKey): T | undefined;
  getSnapshot<T>(key: QueryKey): AsyncState<T> | null;
  invalidate(prefix: readonly unknown[], options?: { refetch?: boolean }): void;
  keys(): QueryKey[];
  set<T>(key: QueryKey, data: T, options?: { updatedAt?: number }): void;
  subscribe(key: QueryKey, listener: () => void): Unsubscribe;
};
type MutationContext = { readonly signal: AbortSignal };
type MutationOptions<T> = {
  invalidateKeys?: readonly (readonly unknown[])[];
  onSuccess?: (data: T, queries: QueryCache) => void | Promise<void>;
  request: (context: MutationContext) => Promise<T>;
  signal?: AbortSignal;
};
type StreamEvent<T = unknown> = { readonly data: T; readonly event: string };
type Unsubscribe = () => void;
ts
type ParamValue = string | number | boolean | null | readonly (string | number | boolean | null)[] | undefined;
type Params = Record<string, ParamValue>;
type RequestConfig<P extends string = string, T = unknown> = {
  body?: unknown;
  fetchInit?: Omit<RequestInit, 'body' | 'headers' | 'method' | 'signal'>;
  headers?: Record<string, string>;
  params?: Record<string, string | number | boolean>;
  query?: Params;
  responseType?: 'auto' | 'json' | 'text' | 'blob' | 'arrayBuffer' | 'raw';
  schema?: { parse(data: unknown): T };
  signal?: AbortSignal;
  timeout?: number;
};

Errors

ErrorTriggerNotable properties
CourierErrorBase class for all Courier errorsUse instanceof to narrow
CourierHttpErrorNon-2xx HTTP responsestatus, data, headers, method, url; CourierHttpError.is(e, status?) narrows by status
CourierNetworkErrorRequest failure without responsemethod, url, cause
CourierTimeoutErrorTimeout signal aborts requestmethod, url, cause
CourierAbortErrorCaller, client, or iterator cancellationmethod, url, cause
CourierSchemaValidationErrorResponse schema failsdata, cause
CourierParseErrorResponse body cannot parse
CourierDisposedErrorWork starts after disposal