# Next.js

Full Next.js integration with apiRoute, createProxyQueryCaller, createCaller for RSC, and createAction for Server Actions.

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`](/docs/deployment/standalone-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

```bash
npx @noverax/create-courier
```

In 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](/docs/create-courier).

## Studio integration & `appOrigin` <a id="studio-app-origin"></a>

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](#step-1-api-route)). 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 — where the Playground sends its proxy calls — lives in `courier.config.ts.runtime.appOrigin` (or set the `COURIER_APP_URL` env var to override per-environment).

```ts title="instrumentation.ts"
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:

```bash
COURIER_APP_URL=http://localhost:3000 next dev
```

Without either, in development the app learns its origin from Next.js itself: the address `next dev` records as it starts listening, `https` only with its own certificate, before any request. No `Host`, `X-Forwarded-Host` or `X-Forwarded-Proto` a request sends changes it, so a `*.localhost` name, a tunnel or a reverse proxy in front needs no setting — see [How the app learns its origin](/docs/api/config#app-origin). Studio shows each runtime with the origin it reports — the server's, and the page's for the browser's — in the sidebar's project card, and the Playground sends proxy calls there.

> **No magic default**
>
> There is no `localhost:3000` auto-fallback. Until the config, the environment or the server itself names the origin, a Playground call in proxy mode throws `ConfigurationError("APP_ORIGIN_NOT_CONFIGURED")` with a pointer to [how the app learns its origin](/docs/api/config#app-origin). 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):

```ts title="instrumentation-client.ts"
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`.

`next start` loads `next.config` too, and Studio is a dev dependency, which an install for production leaves out. Import the wrapper in `next dev` alone:

```ts title="next.config.ts"
import type { NextConfig } from "next"

const nextConfig: NextConfig = {}

export default async function config(phase: string): Promise<NextConfig> {
  // PHASE_DEVELOPMENT_SERVER: next dev alone loads Studio, a dev dependency
  if (phase !== "phase-development-server") return nextConfig
  const { withCourierStudio } = await import("@noverax/courier-studio/next")
  return withCourierStudio()(nextConfig)
}
```

In a `next.config.mjs`, leave out the types; the phase is written out because an ES module config of Next.js 14 cannot import `next/constants`.

## Installation

```bash
npm install @noverax/courier @noverax/courier-next @noverax/courier-react @tanstack/react-query
```

## Define your API sources

Create the API definition the server's callers, the proxy route and Studio share. A page never imports it: it takes the routes Courier writes from it, so the definition stays out of the browser bundle.

```ts title="lib/sources.ts"
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",
  // A page may call these through the proxy; an endpoint no mark opens stays the server's
  expose: true,
  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](#studio-app-origin), which does the same and attaches the runtime in development:

```ts title="instrumentation.ts"
import { registerSources } from "@noverax/courier"

export async function register() {
  const { sources } = await import("./lib/sources")
  registerSources(sources)
}
```

```ts title="app/api/request/route.ts"
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)

List your sources in `courier.config.ts`. From the endpoints they mark `.expose()`, Courier writes the routes a page's caller takes into `courier.routes.ts` beside it — Studio whenever it loads the config, or `npx @noverax/courier-cli routes` now; see [Exposing Endpoints](/docs/proxy-mode/expose):

```ts title="courier.config.ts"
import { defineConfig } from "@noverax/courier/config"
import { sources } from "./lib/sources"

export default defineConfig({ sources })
```

Create a query caller for use in client components, from the routes of `myApi`:

```ts title="lib/query.ts"
import { createProxyQueryCaller } from "@noverax/courier-react"
import { myApiRoutes } from "@/courier.routes"

export const q = createProxyQueryCaller(myApiRoutes)
```

Use it in client components:

```tsx title="app/users/page.tsx"
"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](/docs/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:

```ts title="lib/server-caller.ts"
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](#the-users-session).

```tsx title="app/dashboard/page.tsx"
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.

### One read per render

A page often reads the same thing in several places: `generateMetadata` reads the user for the title, the page reads them for the body, a component reads them again. Within one render, a read that sends the same request again is answered with the first one's answer, however far apart the two are, each caller getting a copy of its own:

```tsx title="app/users/[id]/page.tsx"
import { caller } from "@/lib/server-caller"

type Props = { params: Promise<{ id: string }> }

export async function generateMetadata({ params }: Props) {
  const user = await caller.users.getById({ id: Number((await params).id) })
  return { title: user.name }
}

export default async function UserPage({ params }: Props) {
  // Answered from the read generateMetadata made: the API is asked once
  const user = await caller.users.getById({ id: Number((await params).id) })
  return <h1>{user.name}</h1>
}
```

The render's reads are kept by React's `cache()`, so they last one request and are never shared between users. A read is the same only when everything it sends is — the variables, and every header, the user's session cookies among them — so a read made after the API renewed the session goes out with the new cookies. A write made during the render forgets every read, and what is read after it is read anew; a read that failed is not kept, nor one that asks `coalesce: false`. Route Handlers and Server Actions run outside a render, and each of their reads goes out. Identical reads made at the same moment share one request anywhere — see [Deduplication & Abort](/docs/advanced/abort).

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

```ts title="lib/sources.ts"
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](/docs/server-side/session) 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:

```ts title="proxy.ts"
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`:

```ts title="proxy.ts"
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:

```ts title="lib/actions.ts"
import { createAction } from "@noverax/courier-next"
import { sources } from "@/lib/sources"

export const action = createAction(sources, "myApi")
```

```ts title="app/actions/users.ts"
"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:

```tsx title="app/users/create-form.tsx"
"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

```text title="Project structure"
courier.config.ts         # defineConfig({ sources })
courier.routes.ts         # written by Studio from the .expose() marks
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
  query.ts                # createProxyQueryCaller(myApiRoutes)
  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.
