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 → backendThere 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
| Feature | Proxy Mode | Direct Mode |
|---|---|---|
| Execution | Browser -> proxy handler -> backend | Server -> backend |
| Latency | Extra HTTP hop | Single hop |
| Client bundle | Includes request code | Zero bytes |
| React Query | Built-in hooks | Not needed (async/await) |
| The user's session | The API's session, from the browser's request | The API's session, from the request being served |
| Use case | Client components | RSC, Server Actions, API routes |
| Requires proxy handler | Yes | No |
Quick setup
Create a caller by passing your sources to createCaller:
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:
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:
"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:
import { createServerCaller } from "@noverax/courier-next"
import { api } from "@/api"
export const server = createServerCaller(api)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.headersstay server-side - Source code for
createCalleris 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
- createCaller — full API reference, defaults, and advanced usage
- Server Actions — type-safe mutations with
createAction - User Sessions — the user's session, carried to the API
- Proxy Mode Overview — client-side requests through the proxy handler