Courier has first-class Next.js support through the @noverax/courier-next package. It covers every Next.js pattern: API routes, client components with React Query, server components, and server actions.
WebSockets require a standalone server
Next.js App Router cannot host WebSocket upgrades in-process — neither RSC nor route handlers expose the underlying TCP socket. If your API uses WS endpoints, run @noverax/courier-server alongside Next.js and point proxyPath.ws at it (e.g. ws://localhost:3001 in dev, wss://realtime.example.com in prod). HTTP and SSE continue to work in-process via the App Router.
In one command
npx @noverax/create-courierIn a Next.js app it adds what this page sets up by hand — the API route, the registration in instrumentation.ts and Studio's connection on both sides — with a typed example API, its mock, and a page at /courier. See Create Courier.
Studio integration & appOrigin
Wire your Next.js app into Studio with an attachRuntime() call in the register() of instrumentation.ts — the same function that registers your sources (Step 1). In development it loads courier.config.ts from disk, registers its sources, and opens the connection to Studio; in production the registerSources() call before it registers them. The appOrigin field for Playground proxy SSE/WS resolution lives in courier.config.ts.runtime.appOrigin (or set the COURIER_APP_URL env var to override per-environment).
import { registerSources } from "@noverax/courier"
export async function register() {
const { sources } = await import("./lib/sources")
registerSources(sources)
if (process.env.NEXT_RUNTIME !== "nodejs") return;
if (process.env.NODE_ENV !== "development") return;
const { attachRuntime } = await import("@noverax/courier-studio-core/runtime");
await attachRuntime();
}Alternatively, set the COURIER_APP_URL environment variable instead of pinning appOrigin in the config:
COURIER_APP_URL=http://localhost:3000 next devNo magic default
There is no localhost:3000 auto-fallback. If appOrigin is missing, clicking "Subscribe" on an SSE/WS endpoint in Studio Playground proxy mode throws ConfigurationError("APP_ORIGIN_NOT_CONFIGURED") with a pointer back to this section. The correct value depends on your runtime — custom port, Docker host, HTTPS in dev, reverse proxy — and a wrong guess would silently route requests to nowhere.
That connects the server. To see requests the browser makes, connect it too, from instrumentation-client.ts (Next.js 15.3 and later, under Turbopack or webpack):
import { attachRuntime } from "@noverax/courier-studio-core/runtime";
if (process.env.NODE_ENV === "development") {
attachRuntime("next-browser");
}A browser bundle cannot read courier.config.ts: if Studio is not at its default address, add env: { COURIER_STUDIO_URL: "http://localhost:6290" } to next.config.ts. Under webpack, withCourierStudio() from @noverax/courier-studio/next connects the browser without the file, and keeps ws, which the server's Studio connection runs on, out of the server bundle; bundled, ws fails on every frame of 48 bytes or more and the server never reaches Studio. Under webpack without the wrapper, add serverExternalPackages: ["ws"] to next.config.ts.
Installation
npm install @noverax/courier @noverax/courier-next @noverax/courier-react @tanstack/react-queryDefine your API sources
Create a shared API definition used by all callers. This file can be imported on both server and client:
import { courier, defineApi, defineGroup } from "@noverax/courier/define"
const { endpoint } = courier()
type User = { id: number; name: string; email: string }
type UserList = { data: User[]; total: number }
const users = defineGroup({
prefix: "users",
endpoints: {
getAll: endpoint.get("").variables<{ page?: number; limit?: number }>().response<UserList>(),
getById: endpoint.get(":id").variables<{ id: number }>().response<User>(),
create: endpoint.post("").variables<{ name: string; email: string }>().response<User>(),
update: endpoint.put(":id").variables<{ id: number; name: string; email: string }>().response<User>(),
remove: endpoint.del(":id").variables<{ id: number }>().response<null>(),
},
})
export const api = defineApi({
baseURL: process.env.API_URL ?? "http://localhost:4000",
groups: { users },
})
export const sources = { myApi: api }Step 1: API Route
Create a Next.js App Router route handler that proxies browser requests to your backend, and register your sources at server start. Without Studio, this is the whole of instrumentation.ts; with it, use the register() under Studio integration, which does the same and attaches the runtime in development:
import { registerSources } from "@noverax/courier"
export async function register() {
const { sources } = await import("./lib/sources")
registerSources(sources)
}import { apiRoute } from "@noverax/courier-next"
export const { GET, POST } = apiRoute()Next.js runs instrumentation.ts once at server start, populating the core global registry that apiRoute() reads at request time. register() runs once in each runtime the app uses, and each runtime has its own registry: a route with export const runtime = "edge" reads the Edge runtime's, so the sources are registered in both. If your sources module imports Node-only code, register an Edge-safe one when process.env.NEXT_RUNTIME === "edge". sseRoute() streams in the Edge Runtime as well as in Node.js. The route handles all Courier proxy requests — parses the incoming POST body, resolves the correct endpoint, calls your backend, and returns the response.
Step 2: Client-side caller (React Query)
Create a query caller for use in client components:
import { createProxyQueryCaller } from "@noverax/courier-react"
import type { api } from "@/lib/sources"
export const q = createProxyQueryCaller<typeof api>("myApi")Use it in client components:
"use client"
import { q } from "@/lib/query"
export default function UsersPage() {
const { data, isLoading } = q.users.useQuery("getAll", {
variables: { page: 1, limit: 10 },
})
const { mutate, isPending } = q.users.useMutation("create", {
onSuccess: (user) => {
console.log("Created:", user.name) // fully typed
},
})
if (isLoading) return <p>Loading...</p>
return (
<div>
<h1>Users ({data?.total})</h1>
<ul>
{data?.data.map((user) => (
<li key={user.id}>{user.name} — {user.email}</li>
))}
</ul>
<button
onClick={() => mutate({ name: "Jane", email: "jane@test.com" })}
disabled={isPending}
>
Add User
</button>
</div>
)
}QueryClientProvider
Wrap your app with QueryClientProvider in your root layout. See the React Query setup page for details.
Step 3: Server Components (createServerCaller)
For React Server Components, use createServerCaller to call your backend directly without the proxy hop:
import { createServerCaller } from "@noverax/courier-next"
import { api } from "@/lib/sources"
export const caller = createServerCaller(api)It is createCaller that also finds the request Next.js is serving: an API that keeps a session gets the user's session from it, and a page whose calls carry one is rendered per request — see the user's session.
import { caller } from "@/lib/server-caller"
export default async function DashboardPage() {
const users = await caller.users.getAll({
page: 1,
limit: 5,
})
return (
<div>
<h1>Dashboard ({users.total} users)</h1>
<ul>
{users.data.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
)
}No proxy overhead
createServerCaller calls your backend directly from the server. There is no round-trip through the API route, making it ideal for SSR and RSC.
The user's session
Describe the session the API keeps where it is defined, and every server-side call made for a user carries that user's session, the cookies the API keeps it in and nothing else of your application's:
import { courier, defineApi } from "@noverax/courier/define"
const { endpoint } = courier()
export const api = defineApi({
name: "myApi",
baseURL: process.env.API_URL!,
session: { cookies: ["sessionid"], csrf: { cookie: "csrftoken", header: "x-csrftoken" } },
endpoints: {
me: endpoint.get("me").response<{ id: number; name: string }>(),
},
})| Where | How it finds the request |
|---|---|
| Pages, layouts, Route Handlers | createServerCaller(api) registers nextRequestContext as its module loads |
| Server Actions | createAction reads the action's own request; createServerCaller works too |
| Pages Router | withRequestContext from @noverax/courier-next/pages wraps getServerSideProps and API routes |
| Client components | A proxy caller: the proxy route reads the browser's request |
The session cookies the API sets reach the browser from a Server Action and a Route Handler. Next.js lets no page set cookies while it renders, so a session the API renews during a render reaches that render's later calls only. With cacheComponents, put a component whose calls carry the session under <Suspense>, as you would one that reads cookies(), and keep such calls out of "use cache"; Next.js 14, 15 and 16 are covered alike. User Sessions covers the options and the writes another site sets off.
A page that writes while it renders
A write the API guards with a double-submit token asks for the token first when the user has none, and a page that renders cannot hand it to the browser: the next render asks again. A middleware hands it over with the page instead, where the request does not carry it, and gives it to the render too:
import { csrfTokenMiddleware } from "@noverax/courier-next/middleware"
import { api } from "@/lib/sources"
export const proxy = csrfTokenMiddleware(api)
export const config = { matcher: ["/account/:path*"] }Before Next.js 16 the file is middleware.ts and the export middleware. Import the API's definition alone there, not a module that makes a server caller. Match the pages that write while they render, or every page whose later writes should find the token waiting. A middleware of your own composes it with handCsrfTokens:
import { handCsrfTokens } from "@noverax/courier-next/middleware"
import { type NextRequest, NextResponse } from "next/server"
import { api } from "@/lib/sources"
export async function proxy(request: NextRequest) {
const tokens = await handCsrfTokens(request, api)
const response = NextResponse.next({ request: { headers: tokens.headers } })
response.headers.set("x-frame-options", "DENY")
return tokens.handTo(response)
}Next.js 14, 15 and 16 are checked with it: the token is asked once, the render's write repeats it, and the next visit asks nothing.
Step 4: Server Actions (createAction)
Use createAction for type-safe server actions that read the action's own request — its headers, and the session of an API that keeps one:
import { createAction } from "@noverax/courier-next"
import { sources } from "@/lib/sources"
export const action = createAction(sources, "myApi")"use server"
import { revalidatePath } from "next/cache"
import { action } from "@/lib/actions"
export async function createUser(data: { name: string; email: string }) {
const user = await action.users.create(data)
revalidatePath("/users")
return user
}
export async function deleteUser(id: number) {
await action.users.remove({ id })
revalidatePath("/users")
}Use server actions in client components:
"use client"
import { useActionState } from "react"
import { createUser } from "@/app/actions/users"
export function CreateUserForm() {
const [state, formAction, isPending] = useActionState(
async (_prev: unknown, formData: FormData) => {
const result = await createUser({
name: formData.get("name") as string,
email: formData.get("email") as string,
})
return result
},
null
)
return (
<form action={formAction}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<button type="submit" disabled={isPending}>
{isPending ? "Creating..." : "Create User"}
</button>
</form>
)
}Full project structure
app/
api/request/route.ts # apiRoute()
actions/users.ts # server actions with createAction
users/page.tsx # client component with useQuery
dashboard/page.tsx # server component with createServerCaller
lib/
sources.ts # defineApi + defineGroup + endpoint
endpoint.ts # createProxyQueryCaller<typeof api>("myApi")
server-caller.ts # createServerCaller(api)
actions.ts # createAction(sources, "myApi")Use both modes together
Most Next.js apps use all three: createProxyQueryCaller for client components, createServerCaller for server components, and createAction for server actions. They all share the same source definitions, and the same session.