API signature
createCaller accepts your API definition and optional defaults. It returns a typed client with dot-notation access to all endpoint groups:
declare function createCaller<T extends DefinedApi>(
api: T,
defaults?: CallerDefaults
): TypedClient<T>CallerDefaults
The optional second argument configures defaults applied to every request made through this caller:
type CallerDefaults = {
// Request timeout in milliseconds
timeout?: number
// Headers sent with every request
headers?: Record<string, string>
// Request context passed to every endpoint: headers to forward, the request
context?: RequestContext
// Called when a request fails with an HTTPError — an answer with an
// error status, or a network failure. A ValidationError or a
// cancellation the caller asked for is thrown without it.
onError?: (error: HTTPError) => void | Promise<void>
// Called when any request succeeds
onSuccess?: (data: unknown) => void | Promise<void>
// The sockets of this caller's channels and push subscriptions: token,
// reconnect backoff, heartbeat, queue limits — see Streams & Channels
ws?: CallerWSOptions
}These defaults are merged with per-request options. Per-request headers are merged (not replaced). The default timeout is the least specific: a timeout given for the call wins, then the endpoint's own .timeout(), then the default.
import { createCaller } from "@noverax/courier"
import { api } from "@/api"
export const caller = createCaller(api, {
timeout: 10000,
headers: {
"X-Service": "my-app",
},
onError: (error) => {
console.error(`[API] ${error.status}: ${error.message}`)
},
})Global error logging
The onError callback is useful for centralized error logging. It fires before the error propagates to your component, so you can log to Sentry or a custom service without repeating try/catch blocks.
Basic usage
The returned client uses dot-notation — access a group, then call a method:
import { caller } from "@/lib/caller"
export default async function UsersPage() {
// Dot-notation: group.method(variables?)
const users = await caller.users.getAll({ page: 1, limit: 20 })
// ^? UserList — fully typed from your Endpoint definition
const user = await caller.users.getById({ id: 5 })
// ^? User
return (
<div>
<h1>Users ({users.total})</h1>
<ul>
{users.data.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
</div>
)
}Call signatures
The method accepts different argument combinations depending on whether your endpoint requires variables and whether you need to pass context:
// No variables, no context
await caller.users.getAll()
// With variables, no context
await caller.users.getById({ id: 5 })
// No variables, with per-call options
await caller.users.with({ timeout: 2_000 }).me()
// With variables and per-call options
await caller.users.with({ headers: { "x-tenant": "acme" } }).create({ name: "Jane" })The user's session
An API that keeps a session says so where it is defined, with session, and a call made while the server serves a user carries that user's session — the cookies the API keeps it in and nothing else — from the request being served. In a browser the platform sends its own cookies. See User Sessions for the options, the setup per framework, and how writes another site sets off are refused.
import { createServerCaller } from "@noverax/courier-next"
import { api } from "@/api"
// On Next.js: a caller whose calls carry the session of the request being served
export const server = createServerCaller(api)Context parameter
A caller can also be handed the request it serves, through the context default of createCaller: its session is then read from that request, and its headers pass the whitelist below. A method takes its variables as its only argument; a second argument is not read.
// Exported from "@noverax/courier"
type RequestContext = {
req?: Request // Web Standard request object
res?: Response // Web Standard response object
headers?: Headers // Headers to forward (cookies, auth)
}In React Server Components
A Server Component calls through the caller createServerCaller made: the session of the request Next.js is serving travels with each call, and the page is rendered per request.
import { server } from "@/lib/api.server"
export default async function ProfilePage() {
// The user's session cookies — and nothing else of theirs — reach the API
const profile = await server.users.me()
return <h1>Welcome, {profile.name}</h1>
}Handing a call await headers() whole — .with({ headers: await headers() }) — sends the API every header the browser addressed to your application, every cookie of it included: its own session, analytics, preferences. Describe the session in the API instead.
In API routes
In route handlers, give a caller the Request as its context. Courier extracts the headers from it, through the whitelist below:
import { NextResponse } from "next/server"
import { createCaller } from "@noverax/courier"
import { api } from "@/api"
export async function GET(req: Request) {
// A caller for this request — its headers are extracted automatically
const caller = createCaller(api, { context: { req } })
const stats = await caller.dashboard.getStats()
return NextResponse.json(stats)
}Header whitelist
Headers carried by a context — context.req or context.headers — are forwarded only when they are in a safe subset: authorization, user-agent, referer, origin and x-request-id, plus the names the source lists in allowedHeaders or the endpoint in .headers(). The user's cookie is forwarded from a context only where it is named that way, and so are the credentials a browser sends by itself — an Authorization in Basic, Digest, Negotiate or NTLM, the answer to your site's own login prompt — while a Bearer token goes on. A client address the incoming request claims — x-forwarded-for, x-real-ip — is not copied; X-Forwarded-For is written from the connection where the runtime knows it. Headers you pass to .with({ headers }) are sent as given, except the ones that describe the incoming connection rather than the request — host, connection, keep-alive, content-length, transfer-encoding, te, trailer, upgrade, expect and any header connection names — which fetch sets for its own.
A header you set yourself — in the caller's headers default or with .with({ headers }) — wins over the forwarded copy of the same name, so a service token is not replaced by the end user's. For the user's session, describe it in the API — see User Sessions — rather than forwarding cookie or await headers() whole.
Raw data mode
By default, the request returns the parsed JSON body. If you need access to the HTTP status code and response headers, use .raw:
// Access status code and response headers
const result = await caller.users.raw.getById({ id: 5 })
console.log(result.status) // 200
console.log(result.headers) // Headers object
console.log(result.data) // User — still fully typedDifference from createProxyCaller
The difference is where the call is made. A proxy caller sends the call to your proxy route, which makes it with the browser's request at hand. createCaller makes the call itself, with the request the server is serving at hand, or with the browser's own cookies in a browser:
// Proxy mode (client components): the proxy route makes the call
const browserCaller = createProxyCaller<typeof api>("api")
await browserCaller.users.getAll({ page: 1 })
// Direct mode (server components): the server makes it, with the user's session
const serverCaller = createServerCaller(api)
await serverCaller.users.getAll({ page: 1 })Both use the same executeRequest core under the hood, and the same session the API describes.
Server-only import
In a Next.js app, never import your server caller in "use client" files: the API definition contains your process.env.API_URL, which would be exposed in the client bundle. A single-page app that calls its API from the browser uses createCaller there on purpose, with an address the browser may know — see User Sessions.
Next Steps
- Server Actions — type-safe mutations with
createAction - User Sessions — the user's session, carried to the API from wherever a call is made
- createProxyCaller — the proxy mode equivalent for client components
- React Query useQuery — use with proxy mode for client-side data fetching