Skip to content

API Overview

SymbolPurposeExecution modeCommon gotcha
createCourier()Creates transport clientSyncDispose only when whole scope ends
request()Sends one HTTP request of any methodAsyncDirect calls never deduplicate
get()Sends a GET, optionally through the explicit cacheAsyncOne key must identify one representation
prefetch()Warms a cached GETAsyncRequest failures resolve after surfacing through tap()
invalidateCache() / clearCache()Removes cached valuesSyncInvalidation matches structured key prefixes
post() / put() / patch() / delete()Common write method conveniencesAsyncUse request() for custom methods
withBearerAuth()Adds authorization middlewareSyncToken provider runs per request
withRequestId()Adds request identifier middlewareSyncDefault generator uses crypto.randomUUID()
withLogging()Logs request result metadataSyncRequires explicit logger; URLs may contain sensitive query values
tap()Observe structured transport eventsSyncObservation only; optional signal owns lifetime

Package Entry Point

ImportPurpose
@vielzeug/courierClient factory, errors, middleware helpers, and public types

Client

createCourier()

ts
createCourier(options?: CourierOptions): Courier;

Returns an HTTP client sharing base URL, default headers, immutable middleware, optional parsed-value cache, timeout, and cancellation lifecycle.

CourierOptions fieldTypeDefaultDescription
baseUrlstring''Prefix for relative request paths
cache.capacitynumber100Maximum cached entries; positive integer or Infinity
cache.ttlMsnumber30_000Default cache freshness; non-negative milliseconds or Infinity
fetchtypeof globalThis.fetchglobalThis.fetchFetch implementation
headersHeadersInit{}Default request headers (immutable; merged lowercase)
middlewarereadonly Middleware[][]Immutable middleware chain configured at construction
timeoutnumber30_000Integer milliseconds from 1 to 2,147,483,647, or Infinity

Returns: Courier.

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

const courier = createCourier({
  baseUrl: 'https://api.example.com',
  middleware: [withBearerAuth('token')],
});
Courier memberSignatureDescription
request<T, P>(path, config?) => Promise<T>Sends one HTTP request; config.method defaults to GET
get<T, P>(path, config?) => Promise<T>Sends GET; cache opts parsed responses into bounded caching
prefetch<T, P>(path, config) => Promise<void>Warms one required structured cache key; failures are tap-observed
invalidateCache(prefix: CourierCacheKey) => voidInvalidates matching settled and pending key prefixes
clearCache() => voidRemoves every cached and pending entry
post<T, P>(path, config?) => Promise<T>Sends POST
put<T, P>(path, config?) => Promise<T>Sends PUT
patch<T, P>(path, config?) => Promise<T>Sends PATCH
delete<T, P>(path, config?) => Promise<T>Sends DELETE
tap(handler, options?: { signal?: AbortSignal }) => () => voidObserves structured request and disposal events
cancelAll() => voidAborts active requests; client remains usable
dispose() => voidFinal teardown; aborts work and marks client unusable
disposedbooleanWhether final disposal occurred
disposalSignalAbortSignalAborts on final disposal

Requests

request()

ts
request<T, P extends string>(path: P, config?: RequestConfig<P, T> & { method?: string }): Promise<T>;

Sends one HTTP request. config.method defaults to GET; pass any method (including custom verbs) to support it consistently.

RequestConfig<P, T> fieldTypeDescription
methodstringHTTP method; defaults to GET
paramsRecord<string, string | number | boolean>Path parameters for {param} placeholders
queryParamsQuery string parameters
bodyunknownBodyInit values pass through; streams use Node duplex; GET/HEAD reject bodies; other values encode as JSON
headersHeadersInitPer-request headers merged with (and overriding) defaults
responseTypeResponseTypeResponse parsing strategy
schema{ parse(data: unknown): T }Optional parsed-response validator; incompatible with responseType: 'raw'
signalAbortSignalExternal abort signal merged with internal cancellation
timeoutnumberInteger milliseconds from 1 to 2,147,483,647, or Infinity; overrides default
fetchInitOmit<RequestInit, 'body' | 'headers' | 'method' | 'signal'>Raw fetch options for advanced use

Returns: Parsed response body (or undefined for empty bodies).

ts
const created = await courier.request<User>('/users', {
  method: 'POST',
  body: { name: 'Ada' },
});

get()

ts
get<T, P extends string>(path: P, config?: GetRequestConfig<P, T>): Promise<T>;

Convenience for request(path, { ...config, method: 'GET' }). A cache descriptor opts a parsed response into the bounded cache. It requires a structured key and may override the default TTL. Raw responses cannot be cached.

ts
const user = await courier.get<User>('/users/{id}', {
  cache: { key: ['users', 1], ttlMs: 10_000 },
  params: { id: 1 },
});

Cache

prefetch()

ts
prefetch<T, P extends string>(path: P, config: PrefetchConfig<P, T>): Promise<void>;

Starts the same cached GET used by get(). config.cache is required; signal, per-call timeout, and responseType: 'raw' are unavailable. Fresh values and pending loads are reused. The promise settles when the attempt finishes. Request failures emit request-error through tap() and resolve without caching data.

invalidateCache() and clearCache()

ts
invalidateCache(prefix: CourierCacheKey): void;
clearCache(): void;

invalidateCache() removes every settled entry whose structured key starts with prefix and prevents matching pending loads from being stored. clearCache() applies that behavior to all entries. Existing waiters may still receive an invalidated in-flight result; cancelAll() or dispose() aborts physical work.


Middleware

Middleware is an immutable chain configured at construction. Each middleware receives an immutable FetchContext and a next function.

ts
type Middleware = (ctx: FetchContext, next: (ctx: FetchContext) => Promise<Response>) => Promise<Response>;

Middleware helpers

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

Each helper returns a Middleware passed in the middleware option. withLogging requires an explicit logger function, isolates logger failures from requests, and has no default console output.

ts
const courier = createCourier({
  middleware: [
    withBearerAuth(() => tokenStore.getAccessToken()),
    withRequestId(),
    withLogging({ logger: (msg) => console.log(msg) }),
  ],
});

Types

ts
type TransportOptions = {
  baseUrl?: string;
  fetch?: typeof globalThis.fetch;
  headers?: HeadersInit;
  middleware?: readonly Middleware[];
  timeout?: number;
};

type CourierCacheKeyAtom = string | number | boolean | null;
type CourierCacheKey = readonly [CourierCacheKeyAtom, ...CourierCacheKeyAtom[]];

type CourierCacheOptions = {
  capacity?: number;
  ttlMs?: number;
};

type CourierReadCache = {
  key: CourierCacheKey;
  ttlMs?: number;
};

type CourierOptions = TransportOptions & {
  cache?: CourierCacheOptions;
};

type FetchContext = {
  readonly headers: Readonly<Record<string, string>>;
  readonly init: Readonly<Omit<RequestInit, 'headers'>>;
  readonly url: string;
  withHeaders(updates: Record<string, string | undefined>): FetchContext;
  withInit(updates: Partial<Omit<RequestInit, 'body' | 'headers' | 'method' | 'signal'>>): FetchContext;
};

type Middleware = (ctx: FetchContext, next: (ctx: FetchContext) => Promise<Response>) => Promise<Response>;

type CourierEvent =
  | { type: 'request-start'; method: string; url: string }
  | { type: 'request-success'; method: string; url: string; status: number; duration: number }
  | { type: 'request-error'; method: string; url: string; error: unknown }
  | { type: 'dispose' };
ts
type ParamValue = string | number | boolean | null | readonly (string | number | boolean | null)[] | undefined;
type Params = Record<string, ParamValue>;
type ResponseType = 'auto' | 'json' | 'text' | 'blob' | 'arrayBuffer' | 'raw';
type RequestConfig<P extends string = string, T = unknown> = {
  body?: unknown;
  method?: string;
  fetchInit?: Omit<RequestInit, 'body' | 'headers' | 'method' | 'signal'>;
  headers?: HeadersInit;
  params?: Record<string, string | number | boolean>;
  query?: Params;
  responseType?: ResponseType;
  schema?: { parse(data: unknown): T };
  signal?: AbortSignal;
  timeout?: number;
};

type GetRequestConfig<P extends string = string, T = unknown> = Omit<
  RequestConfig<P, T>,
  'body' | 'method' | 'responseType'
> &
  ({ cache?: CourierReadCache; responseType?: Exclude<ResponseType, 'raw'> } | { cache?: never; responseType: 'raw' }) & {
    body?: never;
  };

type PrefetchConfig<P extends string = string, T = unknown> = Omit<
  GetRequestConfig<P, T>,
  'cache' | 'responseType' | 'signal' | 'timeout'
> & {
  cache: CourierReadCache;
  responseType?: Exclude<ResponseType, 'raw'>;
  signal?: never;
  timeout?: never;
};

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 or client cancellationmethod, url, cause
CourierSchemaValidationErrorResponse schema failsdata, cause
CourierParseErrorResponse body cannot parse
CourierDisposedErrorWork starts after disposal

Errors thrown directly by custom middleware are preserved rather than reclassified.