Skip to content

Server-SidecreateCaller

createCaller

Factory function for server-side calls — call your backend directly from any server runtime.

5 min readdirect mode

API signature

createCaller accepts your API definition and optional defaults. It returns a typed client with dot-notation access to all endpoint groups:

@noverax/courier
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:

ts
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.

lib/caller.ts
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:

app/users/page.tsx
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:

ts
// 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.

lib/api.server.ts
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.

ts
// 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.

app/profile/page.tsx
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:

app/api/dashboard/route.ts
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:

ts
// 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 typed

Difference 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:

ts
// 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