Import
import { createProxyCaller } from "@noverax/courier"Signature
createProxyCaller takes the name the proxy addresses the source by, and defaults — optional for an API of plain requests, required with a boundary (or the source) for one with streams or channels — and returns a typed client. The type argument is your typeof api: it gives the client its groups, methods, variables, and response types.
import type { InferClient, ProxyCallerArguments } from "@noverax/courier"
declare function createProxyCaller<TApi>(
source: string,
...defaults: ProxyCallerArguments<TApi>
): InferClient<TApi>Parameters
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 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.
WS has no default path
proxyPath.ws has no built-in default. Calling a WS endpoint without it throws ConfigurationError("WS_PATH_NOT_CONFIGURED", ...). In runtimes that cannot host WebSocket upgrades in-process (Next.js App Router, Cloudflare Workers, Vercel Edge, AWS Lambda), point this at a running @noverax/courier-server instance.
Return Type
Returns InferClient<typeof api> — a typed object with dot-notation access to your API groups and endpoints:
- Group access —
caller.users— accesses the endpoint group. - Endpoint execution —
caller.users.getById({ id: "123" })— executes an endpoint with its variables, its only argument; an endpoint without variables takes none. - Per-call options —
caller.users.with({ timeout: 5_000 }).getAll()— headers, a timeout or a signal for the calls made through it;.rawresolves to the whole response instead of its body.
import type { InferClient } from "@noverax/courier"
import type { api } from "@/api"
type Client = InferClient<typeof api>Basic Usage
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" })Configuring Defaults
const caller = createProxyCaller<typeof api>("api", {
boundary,
// Per-transport proxy endpoint paths.
// Either side may be partial — missing transports fall back to
// source.options.proxyPath, then to built-in defaults.
proxyPath: {
http: "/api/courier", // override default /api/request
sse: "/api/sse", // default
ws: "wss://realtime.example.com", // required for WS endpoints
},
// Request timeout in milliseconds
timeout: 15000,
// Cookie handling (default: "include")
credentials: "include",
// Default headers for every request
headers: {
"X-App-Version": "1.0.0",
"Accept-Language": "en",
},
// Global error handler
onError: (error) => {
if (error.status === 401) {
window.location.href = "/login"
}
},
// Global success handler
onSuccess: (data) => {
console.log("Request succeeded:", data)
},
})Per-Request Options
Name CallOptions with .with(), then call the endpoint:
const user = await caller.users
.with({
timeout: 5000,
signal: controller.signal,
method: "POST", // Keep this call's variables out of the address
})
.getById({ id: "123" })Upload Progress
Pass onUploadProgress to track file upload progress. When set, Courier uses XMLHttpRequest for real network-level progress tracking.
const result = await caller.files
.with({
onUploadProgress: (progress) => {
console.log(`${Math.round(progress.percent * 100)}%`)
},
})
.upload(formData)XHR Transport
When onUploadProgress is set, Courier uses XMLHttpRequest instead of fetch to provide real network-level progress tracking on both HTTP/1.1 and HTTP/2.
Files go in a FormData. Any other variables travel to the proxy as JSON, so a call whose variables hold a Blob, an ArrayBuffer, a typed array or a stream is refused before it is sent, with ConfigurationError("PROXY_VARIABLE_NOT_SERIALIZABLE", ...) naming the value — see the upload guide.
The progress covers the browser's upload to the proxy route. The proxy then forwards the whole file to your backend, so the call resolves after progress has reached 100% — see progress in proxy mode.
GET or POST
A read — an endpoint built from a reading verb, or declared .reads() — goes to the proxy as GET, its variables as JSON in the _v parameter of the address, so a browser or a CDN can reuse the answer its .cache() allows. A write goes as POST, its variables in the body. A client that knows neither — built without source or boundary — sends every call as POST.
// GET /api/request?_s=api&_g=users&_m=search&_v={"q":"ada"}
const found = await caller.users.search({ q: "ada" })
// POST /api/request?_s=api&_g=users&_m=create, body {"variables":{"name":"Jane"}}
const user = await caller.users.create({ name: "Jane" })A read goes as POST instead when its variables carry a body — form data, an upload whose progress is watched — or would make the address longer than 2 KB, which some servers and CDNs refuse. An address is written down by every access log it passes, so a read whose variables must not be — an email address, a search someone typed — declares sensitiveVariables and always goes as POST; the proxy refuses it over GET:
byEmail: endpoint.get("customers", { sensitiveVariables: true }).variables<{ email: string }>()A client built from a boundary cannot see the definition, so the boundary lists those reads under sensitive. A call can still ask for either verb with .with({ method: "GET" }) or .with({ method: "POST" }); a write asked for over GET is refused 405.
Related
- Proxy Mode — architecture and security model
- Server Handler — the server-side handler that processes proxy requests
- createProxyQueryCaller — React Query wrapper built on top of createProxyCaller
- Types — full type definitions