Skip to content

Server-SideServer-Side Usage

Server-Side Usage

Call your backend directly from any server runtime — no proxy hop, no client bundle, zero overhead.

4 min readdirect mode

What is direct mode?

In direct mode, your server-side code calls the backend directly without routing through a proxy handler. The request flow is straightforward:

createCaller → executeRequest → HttpClient → fetch → backend

There is no intermediate HTTP hop, no serialization through a proxy endpoint, and no exposure to the client bundle. Your backend URL, API keys, and other secrets remain on the server.

Same sources, different transport

Direct mode uses the same defineApi and defineGroup definitions as proxy mode. You define your API structure once and use it from both client and server code.

When to use direct mode

Use direct mode whenever your code runs on the server:

  • React Server Components (RSC) — fetch data at render time with zero client JavaScript
  • Server Actions — handle form submissions and mutations without writing API routes
  • API Routes — when your route handler needs to aggregate data from your backend
  • Any server runtime — Node.js, Deno, Bun, or edge runtimes with fetch support

Benefits over proxy mode

  • Lower latency — eliminates the extra HTTP hop through the proxy endpoint
  • Server-only secrets — backend URLs and API keys never appear in the client bundle
  • No client JavaScript — RSC data fetching adds zero bytes to the browser bundle
  • Simpler error handling — errors stay on the server, no need to serialize them for the client
  • Universal — works in any runtime with native fetch (Node.js, Deno, Bun, edge)

Proxy vs Direct mode

FeatureProxy ModeDirect Mode
ExecutionBrowser -> proxy handler -> backendServer -> backend
LatencyExtra HTTP hopSingle hop
Client bundleIncludes request codeZero bytes
React QueryBuilt-in hooksNot needed (async/await)
The user's sessionThe API's session, from the browser's requestThe API's session, from the request being served
Use caseClient componentsRSC, Server Actions, API routes
Requires proxy handlerYesNo

Quick setup

Create a caller by passing your sources to createCaller:

lib/caller.ts
import { createCaller } from "@noverax/courier"
import { api } from "@/api"

export const caller = createCaller(api)

Usage in Server Components

Call the caller in any async server component. The data is fetched at render time on the server:

app/dashboard/page.tsx
import { caller } from "@/lib/caller"

export default async function DashboardPage() {
  const users = await caller.users.getAll({ page: 1, limit: 10 })

  return (
    <ul>
      {users.data.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}

Usage in Server Actions

Server Actions use the same caller. A write made from the application's own form carries the user's session, and the double-submit token the API asks for:

app/actions/users.ts
"use server"

import { revalidatePath } from "next/cache"
import { server } from "@/lib/api.server"

export async function createUser(data: { name: string; email: string }) {
  const user = await server.users.create(data)

  revalidatePath("/dashboard")
  return user
}

The user's session

The server's own fetch carries none of the user's cookies. An API that keeps a session describes it with session in defineApi, and a call made while the server serves a user carries that user's session from the request being served — the cookies the API keeps it in, and nothing else of the application's:

lib/api.server.ts
import { createServerCaller } from "@noverax/courier-next"
import { api } from "@/api"

export const server = createServerCaller(api)
app/profile/page.tsx
import { server } from "@/lib/api.server"

export default async function ProfilePage() {
  const profile = await server.users.me()

  return <h1>Welcome, {profile.name}</h1>
}

User Sessions covers the options, the setup for each framework, the cookies the API sets, and the writes another site sets off.

Header whitelist

Headers read from a context — createCaller(api, { context: { req } }) — are forwarded only when they are safe: authorization, user-agent, referer, origin and x-request-id, plus the names a source lists in allowedHeaders or an endpoint in .headers() — cookie among them when the user's session should travel. An Authorization the browser sent by itself, in Basic, Digest, Negotiate or NTLM, goes on only where authorization is named that way. A client address is not copied from the incoming request; X-Forwarded-For is written from the connection where the runtime knows it. Headers passed to .with({ headers }) and custom headers from defaults.headers are always included, except those describing the incoming connection, such as host, connection and content-length.

Security considerations

Called from server code, direct mode keeps everything on the server:

  • Your backend URL (API_URL) is never sent to the browser
  • API keys and tokens in defaults.headers stay server-side
  • Source code for createCaller is tree-shaken from the client bundle
  • Error details (stack traces, internal URLs) never leak to the client

Don't import caller in client components

If you import your caller in a "use client" file, your bundler will try to include your source definitions (including process.env.API_URL) in the client. Keep caller imports in server-only files.

Next Steps