Overview
Courier provides two ways to call your API endpoints. Both share the same TypedClient interface and the same type inference:
| Factory | Mode | Use Case |
|---|---|---|
createCaller(api) | Direct | Server-side, Node.js, Bun, Deno, scripts |
createProxyCaller<typeof api>("api") | Proxy | Browser → your server → backend |
createCaller (Direct Mode) direct mode
Calls your backend directly. Works in any JavaScript runtime with fetch.
import { createCaller } from "@noverax/courier"
import { api } from "@/api/sources"
export const caller = createCaller(api)// Works anywhere: Node.js, Bun, Deno, Server Components, scripts
const users = await caller.users.getAll()
// ^? User[]
const user = await caller.users.getById({ id: "1" })
// ^? UserWhen to use direct mode
Use direct mode for server-side code where you have access to the api object: Server Components, Server Actions, API routes, CLI scripts, cron jobs.
createProxyCaller (Proxy Mode) proxy mode
Sends requests through your server's proxy route. The browser never sees your backend URL.
import { createProxyCaller } from "@noverax/courier"
import type { api } from "@/api/sources"
import { boundary } from "@/api/boundary"
export const caller = createProxyCaller<typeof api>("api", {
boundary,
proxyPath: { http: "/api/request" }, // optional, defaults to /api/request
timeout: 10000,
credentials: "include", // send the browser's cookies to the proxy route
headers: { "X-Custom": "value" },
onError: (error) => console.error(error),
onSuccess: (data) => console.log(data),
})// Browser sends GET /api/request → your server → backend
const users = await caller.users.getAll()Source object passed in
Pass the actual api object to createProxyCaller (not just typeof api). The caller reads api.options.proxyPath as source-level defaults and uses the embedded endpoint metadata for SSE/WS transport detection.
ProxyCallerDefaults
| Option | Type | Default | Description |
|---|---|---|---|
proxyPath | ProxyPath | per-transport defaults | Per-transport proxy endpoint paths (http / sse / ws) |
source | object | — | The defineApi() result. Required for SSE/WS detection |
timeout | number | — | Request timeout in ms |
credentials | RequestCredentials | "include" | Whether the browser sends its cookies to the proxy route; the proxy forwards them where the source or endpoint names cookie |
headers | Record<string, string> | — | Default headers |
onError | (error: HTTPError) => void | — | Global error callback |
onSuccess | (data: unknown) => void | — | Global success callback |
ProxyPath shape:
| Field | Default | Notes |
|---|---|---|
proxyPath.http | "/api/request" | HTTP proxy endpoint |
proxyPath.sse | "/api/sse" | SSE proxy endpoint |
proxyPath.ws | none — required for WS endpoints | Use absolute wss:// URL for Next.js / serverless. See standalone server. |
The Dot-Notation Interface
Both factories return a typed client with dot-notation access:
const caller = createCaller(api)
// Access endpoints via group.method notation
const users = await caller.users.getAll()
const user = await caller.users.getById({ id: "1" })Call Options
Per-call options are named with .with(), which returns a caller carrying them:
// Stream mode — the body arrives as a ReadableStream instead of parsed
const response = await caller.events.with({ stream: true }).subscribe({ channel: "builds" })
// A read goes to the proxy as GET; POST keeps this call's variables out of the address
const users = await caller.users.with({ method: "POST" }).getAll({ page: 1 })
// Upload progress tracking
await caller.files
.with({
onUploadProgress: ({ percent }) => console.log(`${Math.round(percent * 100)}%`),
})
.upload(formData)
// Abort signal
const controller = new AbortController()
await caller.users.with({ signal: controller.signal }).getAll()Using Both Modes Together
Most apps use both modes — proxy for the browser, direct for the server. Keep them in separate modules: the server's caller holds the API definition, and a browser module that imported it would ship the definition with the page.
import { createCaller } from "@noverax/courier"
import { api } from "@/api/sources"
// Server-side (RSC, Server Actions, API routes)
export const serverCaller = createCaller(api)import { createProxyCaller } from "@noverax/courier"
import type { api } from "@/api/sources"
import { boundary } from "@/api/boundary"
// Client-side (React components): the API as a type, the boundary as the value
export const clientCaller = createProxyCaller<typeof api>("api", { boundary })See defineBoundary for what the boundary names and why the API is imported as a type.
Next Steps
- Framework Guides — complete setup for your framework
- React Query — hooks with full type inference
- Error Handling — handling HTTPError