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.

15 exportsv0.0.0
ENTRY POINTS
from "@noverax/courier"createCaller, createProxyCaller, createHandler, HTTPError, isHTTPError, registerSources
from "@noverax/courier/define"courier, defineApi, defineGroup, defineBoundary, formBody, formQuery
from "@noverax/courier/config"defineConfig — the shape of courier.config.ts
from "@noverax/courier/middleware"defineMiddleware, composeMiddleware
from "@noverax/courier/realtime"createMemoryPubSub, createMemoryStream, createMemoryPresence, subscribeSSE, tracked
from "@noverax/courier/openapi"toOpenAPI — the definition as an OpenAPI document
from "@noverax/courier/webhooks"defineWebhook and the signature schemes — typed incoming webhooks
from "@noverax/courier-mcp"createMcpHandler, and the courier-mcp command — the endpoints a project offers to agents, as MCP tools
from "@noverax/courier/testing"createTestClient, findRequests, createMockSSESubscription, createMockWSChannel
from "@noverax/courier-react"createQueryCaller, createProxyQueryCaller, createQueryKeys, useConnectionStatus
from "@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 server
from "@noverax/courier-redis"createRedisPubSub, createRedisStream, createRedisPresence

defineApi

FUNCTION

Creates a data source with base URL, options, middleware, and endpoint groups.

SIGNATURE
declare function defineApi<G extends OwnGroups>(
  options: BaseOptions & { groups: G }
): DefinedApi<G>
PARAMETERS1 field
BaseOptions

The 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
}
EXAMPLE
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 sources
The full page

defineGroup

FUNCTION

Creates a typed endpoint group with a shared URL prefix and endpoint definitions.

SIGNATURE
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>
PARAMETERS1 field
DefineGroupConfig
type 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 with defineGroup, to any depth: see Groups inside groups. A group made only of groups needs no endpoints.
  • endpoints — record of typed endpoint functions. Any name but options, which holds the group's own settings, and raw or with, which every client answers with its own .raw and .with(); an endpoint of one of those names is a type error and throws ConfigurationError("RESERVED_ENDPOINT_NAME"). The same holds for the endpoints given to defineApi, whose groups cannot be named raw or with either. A path called options or raw is fine: name the endpoint getOptions or getRaw. See Reserved names.
EXAMPLE
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>(),
  },
})
The full page

defineMiddleware

FUNCTION

Build a named middleware that runs before a call, after it, or around it, for a whole source, a base or one endpoint.

SIGNATURE
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): Middleware
PARAMETERS3 fields
name

A string: what Studio calls the middleware. Its Middleware view, each request's pipeline and its timings show it under this name.

phase

When 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 answer
  • after — receives the answer and returns it, changed or not
  • wrap — calls next() once, more than once or not at all, and returns an answer; for timing, retries, short-circuiting and error handling
handler

The 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 a before or wrap handler may replace
  • ctx.headers — the request's headers, to read or set
  • ctx.transport — "http" for a request, "sse" or "ws" for a stream or a channel
  • ctx.kind — whether the endpoint reads or writes
  • ctx.signal — fires when the caller stops waiting
  • ctx.request — the incoming request, when a proxy runs the call
  • response — { status, data, headers }: the answer an after handler receives and next() resolves to
EXAMPLE
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
})
The full page

createCaller

FUNCTION

Factory function that creates a typed caller for server-side direct HTTP calls — no proxy hop needed.

SIGNATURE
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>
PARAMETERS2 fields
api

An API source definition created with defineApi().

import { api } from "@/api"

const caller = createCaller(api)
CallerDefaults

Optional 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.
EXAMPLE
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" })
The full page

createProxyCaller

FUNCTION

Factory function that creates a typed client with dot-notation access for proxy-mode HTTP calls from the browser.

SIGNATURE
import type { InferClient, ProxyCallerArguments } from "@noverax/courier"

declare function createProxyCaller<TApi>(
  source: string,
  ...defaults: ProxyCallerArguments<TApi>
): InferClient<TApi>
PARAMETERS1 field
ProxyCallerDefaults

All 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 against location.origin) or an absolute URL. Caller-level fields are merged per-transport over source.options.proxyPath from defineApi.
  • source — the result of defineApi(), or boundary — the result of defineBoundary(), 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 reads proxyPath from 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 when source is 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 names cookie — 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 every HTTPError.
  • 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.
EXAMPLE
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" })
The full page

createQueryCaller

FUNCTION

Creates a direct-mode React Query client that connects straight to the backend, no proxy adapter required.

SIGNATURE
declare function createQueryCaller<TApi extends DefinedApi>(
  api: TApi,
  defaults?: QueryCallerDefaults,
): QueryCallerClient<TApi>
The full page

createProxyQueryCaller

FUNCTION

Creates a proxy-mode React Query client with dot-notation hooks for every endpoint group, routed through the server adapter.

SIGNATURE
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>
The full page

createQueryKeys

FUNCTION

Build the React Query keys the hooks use, typed by your API, for invalidation, prefetching and optimistic updates.

SIGNATURE
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>
The full page

createTestClient

FUNCTION

An in-memory client for tests: typed like your API, answered by mocks, recording every call — no HTTP.

SIGNATURE
import type { DefinedApi } from "@noverax/courier/define"
import type { TestClient } from "@noverax/courier/testing"

declare function createTestClient<TApi extends DefinedApi = DefinedApi>(): TestClient<TApi>
PARAMETERS6 fields
client

a typed client, with the same dot-notation, .raw and .with() as createCaller; streams and channels answer as their mocks say

mock

mock(group, method), checked against the API, returns a builder typed by the endpoint's response and variables: resolves, returns, rejects, stream, channel, once, times, delay

history

every call, in order, with its group, method, variables, result or error, and whether it was a stream or a channel

reset

clears the mocks and the history

resetMocks

clears the mocks only

resetHistory

clears the history only

EXAMPLE
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")
})
The full page

createHandler

FUNCTION

Universal adapter factory that creates Web Standard handlers for proxy-mode requests from any framework.

SIGNATURE
declare function createHandler(config?: HandlerConfig): AdapterHandler
PARAMETERS2 fields
config.sources

A record of API source definitions created with defineApi(). Given, the handler answers for those sources alone, and a later registerSources() in the same process does not change what it serves. Left out, the handler reads the process-wide registry that registerSources() and attachRuntime() 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 gets 421 — see Host validation | | maxBodyBytes | 1 MB | Largest JSON envelope the handler reads; a larger one is refused with 413 | | maxUploadBytes | 10 MB | Largest multipart/form-data upload 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 with Last-Event-ID | | maxSSEConnections | unlimited | Most SSE streams open at once | | onSSEConnect, onSSEDisconnect, onSSEError | — | SSE lifecycle hooks |

The full page

HttpClient

TYPE

Internal HTTP client that wraps native fetch with typed methods, automatic serialization, and path parameter resolution.

SIGNATURE
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>>
The full page

HTTPError

CLASS

Custom error class for HTTP responses with typed data, status code, and headers. Includes the isHTTPError type guard.

SIGNATURE
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 })
}
PARAMETERS5 fields
status

the HTTP status code. 0 for network errors (DNS failure, connection refused).

headers

the response Headers object. Empty Headers for network errors.

data

the parsed response body. Typed as TData (defaults to unknown).

name

always "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".

EXAMPLE
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 full page

CallOptions

TYPE

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.

SIGNATURE
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">
PARAMETERS10 fields
signal

aborts the call when it fires

timeout

milliseconds the call may take; a call's own wins over the source's

headers

added to the call's headers, name by name, whatever their case; .with() at the client, a group and an endpoint merges them all

stream

answers with the response's body as a stream, unread, instead of the parsed data

forwardVariables

whether 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; false leaves them to the endpoint's own .via() code. Defaults to true

method

the 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 address

responseType

how a successful answer is read, when not as the source reads every answer

onUploadProgress

called as the body is sent, with percent from 0 to 1, transferredBytes and totalBytes

onDownloadProgress

called as the answer is read, in the same terms

RequestInit

the rest of fetch's own options, cache, credentials, keepalive, mode, priority, redirect, referrer, referrerPolicy and integrity, passed to it as they are

EXAMPLE
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 })
The full page

Types

TYPE

Complete reference for all TypeScript types exported by @noverax/courier.

SIGNATURE
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>
}
The full page

courier.config.ts

FILE

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.

SIGNATURE
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" },
})
The full page

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.

GETendpoint.get(path)A read; variables not in the path go to the query string.
HEADendpoint.head(path)A read answered with headers and no body.
OPTIONSendpoint.options(path)What the address allows.
POSTendpoint.post(path)A write; variables not in the path go in the body.
PUTendpoint.put(path)Replaces the resource at the address.
PATCHendpoint.patch(path)Changes part of the resource at the address.
DELETEendpoint.del(path)Deletes it; variables not in the path go to the query string. Named del() as delete is reserved.
STREAMstream.get(path)Server-sent events another process serves; .implement() serves them here.
CHANNELchannel.connect(path)A WebSocket channel another process serves; .implement() serves it here.