DocsAPI reference
API reference
Every export the docs describe, read from its page: where to import it from, its signature, the fields it takes and an example. Generics carry through from your definition, so the types here are the types at the call site.
from "@noverax/courier"createCaller, createProxyCaller, createHandler, HTTPError, isHTTPError, registerSourcesfrom "@noverax/courier/define"courier, defineApi, defineGroup, defineBoundary, formBody, formQueryfrom "@noverax/courier/config"defineConfig — the shape of courier.config.tsfrom "@noverax/courier/middleware"defineMiddleware, composeMiddlewarefrom "@noverax/courier/realtime"createMemoryPubSub, createMemoryStream, createMemoryPresence, subscribeSSE, trackedfrom "@noverax/courier/openapi"toOpenAPI — the definition as an OpenAPI documentfrom "@noverax/courier/webhooks"defineWebhook and the signature schemes — typed incoming webhooksfrom "@noverax/courier-mcp"createMcpHandler, and the courier-mcp command — the endpoints a project offers to agents, as MCP toolsfrom "@noverax/courier/testing"createTestClient, findRequests, createMockSSESubscription, createMockWSChannelfrom "@noverax/courier-react"createQueryCaller, createProxyQueryCaller, createQueryKeys, useConnectionStatusfrom "@noverax/courier-next"apiRoute(), sseRoute(), createAction()from "@noverax/courier-nuxt · -hono · -express · -astro · -remix · -solid · -sveltekit"courierHandler()from "@noverax/courier-fastify · -nestjs"courierPlugin, courierMiddleware()from "@noverax/courier-server"createCourierServer — the standalone serverfrom "@noverax/courier-redis"createRedisPubSub, createRedisStream, createRedisPresenceCreates a data source with base URL, options, middleware, and endpoint groups.
declare function defineApi<G extends OwnGroups>(
options: BaseOptions & { groups: G }
): DefinedApi<G>BaseOptionsThe core configuration for a data source:
type BaseOptions = { baseURL?: string responseType?: "json" | "text" | "blob" | "arrayBuffer" | "formData" responseParser?: (response: Response) => Promise<unknown> defaultBody?: Record<string, unknown> defaultQueryParams?: Record<string, unknown> allowedHeaders?: string[] bodySerializer?: (data: Record<string, unknown>) => BodyInit searchParamsSerializer?: SearchParamsSerializer deduplicate?: boolean middleware?: Middleware[] pathOptions?: PathOptions timeout?: number headers?: HeadersInit signal?: AbortSignal session?: SessionOptions forward?: ForwardOptions }
import { defineApi } from "@noverax/courier/define"
import { users } from "./users"
import { posts } from "./posts"
const api = defineApi({
baseURL: process.env.API_URL ?? "http://localhost:4000",
groups: { users, posts },
})
export const sources = { api }
export type Sources = typeof sourcesCreates a typed endpoint group with a shared URL prefix and endpoint definitions.
declare function defineGroup<TEndpoints extends OwnEndpoints>(
config: DefineGroupConfig<TEndpoints>
): DefinedGroup<TEndpoints>
declare function defineGroup<TGroups extends OwnGroups, TEndpoints extends OwnEndpoints>(
config: DefineNestingGroupConfig<TEndpoints, TGroups>
): DefinedNestingGroup<TEndpoints, TGroups>DefineGroupConfigtype DefineGroupConfig<E> = GroupOptions & { endpoints: E } type DefineNestingGroupConfig<E, G> = GroupOptions & { endpoints?: E groups: G } type GroupOptions = { prefix?: string pathOptions?: (request?: Request) => Record<string, unknown> | Promise<Record<string, unknown>> expose?: boolean fetch?: SourceFetch webSocket?: SourceWebSocket dispatcher?: SourceDispatcher proxy?: string // Bun tls?: object // Bun client?: object // Deno }prefix— URL path segment prepended to all endpoint URLs in this group.pathOptions— async function that returns dynamic key-value pairs (headers, cookies) to inject per request.expose— whether the browser may call the group through the proxy; it overrides the source's, and a group inside another overrides its outer group's.groups— groups inside this one, created withdefineGroup, to any depth: see Groups inside groups. A group made only of groups needs noendpoints.endpoints— record of typed endpoint functions. Any name butoptions, which holds the group's own settings, andraworwith, which every client answers with its own.rawand.with(); an endpoint of one of those names is a type error and throwsConfigurationError("RESERVED_ENDPOINT_NAME"). The same holds for the endpoints given todefineApi, whose groups cannot be namedraworwitheither. A path calledoptionsorrawis fine: name the endpointgetOptionsorgetRaw. See Reserved names.
import { courier, defineGroup } from "@noverax/courier/define"
const { endpoint } = courier()
type User = { id: string; name: string; email: string }
export const users = defineGroup({
prefix: "users",
endpoints: {
getAll: endpoint.get("").response<User[]>(),
getById: endpoint.get(":id").variables<{ id: string }>().response<User>(),
create: endpoint.post("").variables<{ name: string; email: string }>().response<User>(),
update: endpoint.put(":id").variables<{ id: string; name: string }>().response<User>(),
remove: endpoint.del(":id").variables<{ id: string }>().response<null>(),
},
})Build a named middleware that runs before a call, after it, or around it, for a whole source, a base or one endpoint.
import type { AfterHandler, BeforeHandler, Middleware, WrapHandler } from "@noverax/courier/middleware"
declare function defineMiddleware(name: string, phase: "before", handler: BeforeHandler): Middleware
declare function defineMiddleware(name: string, phase: "after", handler: AfterHandler): Middleware
declare function defineMiddleware(name: string, phase: "wrap", handler: WrapHandler): MiddlewarenameA string: what Studio calls the middleware. Its Middleware view, each request's pipeline and its timings show it under this name.
phaseWhen the handler runs, and so what it is handed.
type MiddlewarePhase = "before" | "after" | "wrap"before— changes the request — its headers, its variables — and lets it go on; it never sees the answerafter— receives the answer and returns it, changed or notwrap— callsnext()once, more than once or not at all, and returns an answer; for timing, retries, short-circuiting and error handling
handlerThe function for that phase, handed the call's context:
type BeforeHandler = (ctx: MiddlewareContext) => void | Promise<void> type AfterHandler = ( ctx: MiddlewareContext, response: MiddlewareResponse, ) => MiddlewareResponse | Promise<MiddlewareResponse> type WrapHandler = ( ctx: MiddlewareContext, next: () => Promise<MiddlewareResponse>, ) => Promise<MiddlewareResponse>ctx.variables— the call's variables, which abeforeorwraphandler may replacectx.headers— the request's headers, to read or setctx.transport—"http"for a request,"sse"or"ws"for a stream or a channelctx.kind— whether the endpoint reads or writesctx.signal— fires when the caller stops waitingctx.request— the incoming request, when a proxy runs the callresponse—{ status, data, headers }: the answer anafterhandler receives andnext()resolves to
import { defineMiddleware } from "@noverax/courier/middleware"
export const timing = defineMiddleware("timing", "wrap", async (ctx, next) => {
const start = performance.now()
const response = await next()
const ms = Math.round(performance.now() - start)
console.log(`${ctx.source}.${ctx.group}.${ctx.method} → ${response.status} in ${ms} ms`)
return response
})Factory function that creates a typed caller for server-side direct HTTP calls — no proxy hop needed.
import type { CallerDefaults, TypedClient } from "@noverax/courier"
import type { DefinedApi } from "@noverax/courier/define"
declare function createCaller<TApi extends DefinedApi>(
api: TApi,
defaults?: CallerDefaults,
): TypedClient<TApi>apiAn API source definition created with
defineApi().import { api } from "@/api" const caller = createCaller(api)CallerDefaultsOptional default settings applied to every request.
type CallerDefaults = { timeout?: number headers?: Record<string, string> context?: Context onError?: (error: HTTPError) => void | Promise<void> onSuccess?: (data: unknown) => void | Promise<void> ws?: CallerWSOptions } type CallerWSOptions = { auth?: WSAuthParams | (() => WSAuthParams | undefined | Promise<WSAuthParams | undefined>) backoff?: { initialMs?: number; maxMs?: number; factor?: number; jitter?: boolean } heartbeat?: { intervalMs?: number; timeoutMs?: number } messageQueue?: { maxCount?: number; maxBytes?: number } protocols?: string[] maxMessageSize?: number } type WSAuthParams = Record<string, unknown>ws— options of the sockets this caller's channels and push subscriptions open; see Connection options.
import { createCaller } from "@noverax/courier"
import { api } from "@/api"
export const caller = createCaller(api, {
timeout: 10000,
})
// Usage: caller.group.method(variables?)
const result = await caller.users.getAll()
const user = await caller.users.getById({ id: "123" })Factory function that creates a typed client with dot-notation access for proxy-mode HTTP calls from the browser.
import type { InferClient, ProxyCallerArguments } from "@noverax/courier"
declare function createProxyCaller<TApi>(
source: string,
...defaults: ProxyCallerArguments<TApi>
): InferClient<TApi>ProxyCallerDefaultsAll fields are optional.
type ProxyPath = { http?: string // default: "/api/request" sse?: string // default: "/api/sse" ws?: string // no default — required for WS endpoints } type ProxyCallerDefaults = { proxyPath?: ProxyPath source?: Record<string, unknown> boundary?: ProxyBoundary timeout?: number deduplicate?: boolean credentials?: RequestCredentials headers?: Record<string, string> onError?: (error: HTTPError) => void | Promise<void> onSuccess?: (data: unknown) => void | Promise<void> ws?: CallerWSOptions } type CallerWSOptions = { auth?: WSAuthParams | (() => WSAuthParams | undefined | Promise<WSAuthParams | undefined>) backoff?: { initialMs?: number; maxMs?: number; factor?: number; jitter?: boolean } heartbeat?: { intervalMs?: number; timeoutMs?: number } messageQueue?: { maxCount?: number; maxBytes?: number } protocols?: string[] maxMessageSize?: number } type WSAuthParams = Record<string, unknown>proxyPath— per-transport proxy endpoint configuration. Each field accepts either a relative path (resolved againstlocation.origin) or an absolute URL. Caller-level fields are merged per-transport oversource.options.proxyPathfromdefineApi.source— the result ofdefineApi(), orboundary— the result ofdefineBoundary(), which keeps the definition out of the browser bundle. One of them is required when the API has streams or channels: the type argument names each endpoint's transport, but only a runtime value tells the client which endpoints to open as a stream or a socket, so omitting both does not compile. The caller readsproxyPathfrom whichever is given as the source-level defaults.deduplicate— cancel a call still in flight when the same endpoint is called again. Defaults to the source's own setting whensourceis given.timeout— how long the browser waits for the proxy, in milliseconds. It is not sent to the server, so the endpoint's own.timeout()still bounds the upstream call; a timeout given with.with()for one call is sent.credentials— whether the browser sends its cookies to the proxy route, with every call and stream. Defaults to"include". The proxy forwards them to the backend only where the source or the endpoint namescookie— see Cookies.headers— default headers merged into every request and stream, and sent in the first frame of every socket its channels open, which the proxy reads as the socket's own — a browser cannot put them on a WebSocket's handshake.onError— global error handler invoked on everyHTTPError.onSuccess— global success handler invoked on every successful response.ws— options of the sockets this caller's channels and push subscriptions open; see Connection options.
import { createProxyCaller } from "@noverax/courier"
import type { api } from "@/api"
import { boundary } from "@/api/boundary"
const caller = createProxyCaller<typeof api>("api", {
boundary,
timeout: 10000,
credentials: "include",
})
// Dot-notation: caller.group.method(variables)
const users = await caller.users.getAll()
const user = await caller.users.getById({ id: "123" })Creates a direct-mode React Query client that connects straight to the backend, no proxy adapter required.
declare function createQueryCaller<TApi extends DefinedApi>(
api: TApi,
defaults?: QueryCallerDefaults,
): QueryCallerClient<TApi>Creates a proxy-mode React Query client with dot-notation hooks for every endpoint group, routed through the server adapter.
import type { ProxyCallerArguments } from "@noverax/courier"
import type { DefinedApi } from "@noverax/courier/define"
import type { QueryCallerClient } from "@noverax/courier-react"
declare function createProxyQueryCaller<TApi extends DefinedApi>(
source: string,
...defaults: ProxyCallerArguments<TApi>
): QueryCallerClient<TApi>Build the React Query keys the hooks use, typed by your API, for invalidation, prefetching and optimistic updates.
import type { DefinedApi } from "@noverax/courier/define"
import type { QueryKeyFactory } from "@noverax/courier-react"
declare function createQueryKeys<TApi extends DefinedApi>(api: TApi): QueryKeyFactory<TApi>
declare function createProxyQueryKeys<TApi extends DefinedApi>(source: string): QueryKeyFactory<TApi>An in-memory client for tests: typed like your API, answered by mocks, recording every call — no HTTP.
import type { DefinedApi } from "@noverax/courier/define"
import type { TestClient } from "@noverax/courier/testing"
declare function createTestClient<TApi extends DefinedApi = DefinedApi>(): TestClient<TApi>clienta typed client, with the same dot-notation,
.rawand.with()ascreateCaller; streams and channels answer as their mocks saymockmock(group, method), checked against the API, returns a builder typed by the endpoint's response and variables:resolves,returns,rejects,stream,channel,once,times,delayhistoryevery call, in order, with its
group,method,variables,resultorerror, and whether it was a stream or a channelresetclears the mocks and the history
resetMocksclears the mocks only
resetHistoryclears the history only
import { beforeEach, expect, it } from "vitest"
import { createTestClient } from "@noverax/courier/testing"
import type { api } from "../lib/api"
const { client, mock, history, reset } = createTestClient<typeof api>()
beforeEach(() => reset())
it("lists the users", async () => {
mock("users", "getAll").resolves([{ id: 1, name: "Ada" }])
const users = await client.users.getAll()
expect(users).toEqual([{ id: 1, name: "Ada" }])
expect(history[0]?.method).toBe("getAll")
})Universal adapter factory that creates Web Standard handlers for proxy-mode requests from any framework.
declare function createHandler(config?: HandlerConfig): AdapterHandlerconfig.sourcesA record of API source definitions created with
defineApi(). Given, the handler answers for those sources alone, and a laterregisterSources()in the same process does not change what it serves. Left out, the handler reads the process-wide registry thatregisterSources()andattachRuntime()fill, at request time — which is how every framework adapter works.Other options| Option | Default | Description | |--------|---------|-------------| |
allowedOrigins| own origin; local ports in development, for an app served locally | Origins of other pages that may call the proxy — see Origin Validation | |allowedHosts| loopback names and IP literals; every name in production | Host names the app is reached by; a request for any other gets421— see Host validation | |maxBodyBytes| 1 MB | Largest JSON envelope the handler reads; a larger one is refused with 413 | |maxUploadBytes| 10 MB | Largestmultipart/form-dataupload the handler reads; a larger one is refused with 413 | |sseHeaders| — | Extra response headers on every SSE stream | |sseMaxLifetimeMs| 30 minutes | Longest an SSE stream stays open before the client reconnects withLast-Event-ID| |maxSSEConnections| unlimited | Most SSE streams open at once | |onSSEConnect,onSSEDisconnect,onSSEError| — | SSE lifecycle hooks |
Internal HTTP client that wraps native fetch with typed methods, automatic serialization, and path parameter resolution.
get: <T>(url?, variables?, options?) => Promise<HttpResponse<T>>
post: <T>(url?, variables?, options?) => Promise<HttpResponse<T>>
put: <T>(url?, variables?, options?) => Promise<HttpResponse<T>>
patch: <T>(url?, variables?, options?) => Promise<HttpResponse<T>>
del: <T>(url?, variables?, options?) => Promise<HttpResponse<T>>
head: <T>(url?, options?) => Promise<HttpResponse<T>>Custom error class for HTTP responses with typed data, status code, and headers. Includes the isHTTPError type guard.
import { CourierError, type HttpResponse } from "@noverax/courier"
declare class HTTPError<TData = unknown> extends CourierError<"HTTP_ERROR"> {
readonly status: number
readonly headers: Headers
readonly data: TData
constructor(response: HttpResponse<TData>, options?: { cause?: unknown; message?: string })
}statusthe HTTP status code.
0for network errors (DNS failure, connection refused).headersthe response
Headersobject. EmptyHeadersfor network errors.datathe parsed response body. Typed as
TData(defaults tounknown).namealways
"HTTPError";code— always"HTTP_ERROR".message"HTTP {status}"(e.g.,"HTTP 404"), or, when there was no answer,"Network request failed"or"Request timed out".
import { isHTTPError } from "@noverax/courier"
try {
const user = await caller.users.getById({ id: "999" })
} catch (error) {
if (isHTTPError(error)) {
console.error(`HTTP ${error.status}:`, error.data)
}
}The options one call takes with .with(): its timeout, headers and signal, upload and download progress, a stream instead of parsed data, and the verb sent to the proxy.
type CallOptions = Omit<RequestInit, "method" | "body" | "signal"> & {
signal?: AbortSignal | null
timeout?: number
stream?: boolean
forwardVariables?: boolean
method?: "GET" | "POST"
responseType?: "json" | "text" | "blob" | "arrayBuffer" | "formData"
onUploadProgress?: (progress: UploadProgress) => void
onDownloadProgress?: (progress: DownloadProgress) => void
rawData?: boolean
}
type WithOptions = Omit<CallOptions, "rawData">signalaborts the call when it fires
timeoutmilliseconds the call may take; a call's own wins over the source's
headersadded to the call's headers, name by name, whatever their case;
.with()at the client, a group and an endpoint merges them allstreamanswers with the response's body as a stream, unread, instead of the parsed data
forwardVariableswhether the HTTP methods an endpoint calls send the call's variables by themselves, as the body of a write or the query of a read;
falseleaves them to the endpoint's own.via()code. Defaults totruemethodthe verb the call goes to the proxy with: GET for a read and POST for a write by default;
"POST"keeps a read's variables out of the addressresponseTypehow a successful answer is read, when not as the source reads every answer
onUploadProgresscalled as the body is sent, with
percentfrom 0 to 1,transferredBytesandtotalBytesonDownloadProgresscalled as the answer is read, in the same terms
RequestInitthe rest of
fetch's own options,cache,credentials,keepalive,mode,priority,redirect,referrer,referrerPolicyandintegrity, passed to it as they are
import { caller } from "@/lib/caller"
const controller = new AbortController()
// A slow endpoint gets more time, and the user can cancel it
const report = await caller.reports
.with({ timeout: 30_000, signal: controller.signal })
.generate({ type: "annual" })
// An upload reports how far it got, from 0 to 1
await caller.files
.with({ onUploadProgress: ({ percent }) => console.log(`${Math.round(percent * 100)}%`) })
.upload({ body: blob })Complete reference for all TypeScript types exported by @noverax/courier.
type TypedClient<T extends DefinedApi<any>, Raw extends boolean = false> = {
[K in keyof ApiGroups<T>]: GroupClient<ApiGroups<T>[K], Raw>
} & {
raw: TypedClient<T, true>
with(options: WithOptions): TypedClient<T, Raw>
}Every field of courier.config.ts: the sources, the API imports Studio keeps in sync, Studio, the standalone server, the runtime and what agents may call.
import { defineConfig } from "@noverax/courier/config"
import { api } from "./api/sources"
export default defineConfig({
sources: { api },
studio: { port: 6280 },
server: { allowedOrigins: ["https://app.example.com"] },
runtime: { appOrigin: "http://localhost:3000" },
})Endpoint builder
Starters returning a chainable descriptor. .variables() and .response() carry the types to every call site; .via(probe) works the address out from the variables.