# Server Handler

The universal server-side handler that receives proxy requests and forwards them to your backend — works with any framework.

## Overview

`createHandler` creates a universal request handler that acts as the server-side half of proxy mode. It receives requests from `createProxyCaller`, validates them, and forwards them to your backend via `executeRequest`.

```typescript
import { createHandler } from "@noverax/courier"

const { handle, handleGet, handlePost } = createHandler({ sources })
// handle:     (request: Request) => Promise<Response>  — auto-routes GET/POST
// handleGet:  (request: Request) => Promise<Response>  — query endpoints
// handlePost: (request: Request) => Promise<Response>  — mutations + data
```

The handler works with Web Standard `Request`/`Response` objects, making it compatible with any JavaScript runtime and framework.

## Setup

### Step 1: Define your sources

The sources object maps source names to API definitions created with `defineApi`:

```ts title="api/index.ts"
import { courier, defineApi, defineGroup } from "@noverax/courier/define"

const { endpoint } = courier()

type User = { id: number; name: string; email: string }
type Post = { id: number; title: string; body: string }

const users = defineGroup({
  prefix: "users",
  endpoints: {
    getAll: endpoint.get("").response<User[]>(),
    getById: endpoint.get(":id").variables<{ id: number }>().response<User>(),
  },
})

const posts = defineGroup({
  prefix: "posts",
  endpoints: {
    list: endpoint.get("").response<Post[]>(),
    create: endpoint.post("").variables<{ title: string }>().response<Post>(),
  },
})

const api = defineApi({
  baseURL: process.env.API_URL ?? "http://localhost:4000",
  groups: { users, posts },
})

export const sources = { api }
export type Sources = typeof sources
```

### Step 2: Create the server handler

Choose the adapter that matches your framework:

**Next.js**

```ts

```

**Express**

```ts

```

**Hono**

```ts

```

**Fastify**

```ts

```

**NestJS**

```ts

```

**Nuxt**

```ts

```

> **All adapters use createHandler**
>
> Every framework adapter is a thin wrapper around `createHandler()` from `@noverax/courier`. Sources are read lazily from the core global registry populated by `registerSources()` (or by `attachRuntime()` auto-loading `courier.config.ts`). If your framework is not listed, use `createHandler` directly with Web Standard `Request`/`Response`.

## How it works

All adapters delegate to `createHandler()`, which returns three methods:

- **`handle(request)`** — auto-routes GET and POST, returns 405 for other methods
- **`handleGet(request)`** — checks the origin, parses query parameters (`_s`, `_g`, `_m` + variables) for cacheable read endpoints
- **`handlePost(request)`** — checks the origin, parses JSON or FormData body for mutations

## Origin validation

Every call the proxy serves — GET and POST here, and the SSE and WebSocket routes too — is checked for where it comes from, so a page on another site cannot drive the proxy with the user's cookies:

- **`Sec-Fetch-Site`** decides when the browser sends it, and a current browser sends it on every request to a secure origin: `same-origin`, and `none` for what the user typed, pass; `same-site` and `cross-site` pass only from an origin listed in `allowedOrigins`.
- **`Origin`** is compared with the origin the request was addressed to where the browser says nothing else; a foreign origin passes only when listed.
- A request with **neither header** — a server, a CDN, a script — is not a browser's cross-origin request, and is served. Limit how often one caller calls with `rateLimit` — see [Rate limits](/docs/proxy-mode/overview#rate-limits).
- In **development**, while `allowedOrigins` is empty, a page on another port of the local machine is admitted when the app itself is served from that machine, so Studio can inspect the app. An app on any other address never admits one — a server started without `NODE_ENV` counts as in development, and a page on a visitor's own machine, a local tool or another dev server, is not the application's. Configuring `allowedOrigins` replaces the allowance with the list.

A refused call is answered `403` with `{ "success": false, "message": "Origin not allowed" }`; a refused socket is closed with `4403`. A pattern is an exact origin, `https://*.example.com` for the sub-domains of a host over one scheme and port, or `*.example.com` for them over any scheme on its default port — plain HTTP included, whose pages anyone on a visitor's network can stand in for, so name the scheme unless such a page must call the proxy:

```ts
import { createHandler } from "@noverax/courier"

const handler = createHandler({
  allowedOrigins: ["https://admin.example.com", "https://*.example.com"],
})
```

An entry a browser could never send — `https://admin.example.com/` with its trailing slash, a path, `:443` on HTTPS — stops the handler from being created, with `INVALID_ALLOWED_ORIGIN` naming the form to use.

## Content-Type handling

The POST handler accepts two content types and rejects everything else:

```typescript
// The handler accepts two content types:

// The endpoint is named in the URL for both: ?_s=api&_g=users&_m=create

// 1. application/json — standard requests
// Body: { variables, options } — any other field is refused with 400

// 2. multipart/form-data — file uploads
// Fields: _options first, then the user's data and files

// Anything else returns 415 Unsupported Media Type
// { success: false, message: "Unsupported Content-Type" }
```

## Cookies

An API that keeps the user's session describes it with [`session`](/docs/server-side/session): the proxy then sends it the cookies the session is kept in and nothing else of the browser's, repeats its double-submit token on writes, and hands the browser the session cookies it sets, made for your application's origin. For an API that keeps none, cookies cross the proxy only where you name them, one direction at a time:

- `cookie` — the browser's cookies are forwarded to the backend.
- `set-cookie` — the cookies the backend sets are passed back to the browser.

Name them where the rest of the header allowlist lives: on the source with `allowedHeaders`, on a base with `courier().extend({ allowedHeaders })`, or on one endpoint with `.headers([...])`. Left unnamed, neither crosses. The browser sends the proxy route every cookie of your application's origin, its own session among them, and a backend a source points at, a third party's included, receives them and sets cookies on your origin only where you say so.

A backend-for-frontend sign-in names the session where it travels:

```ts title="api/index.ts"
import { courier, defineApi, defineGroup } from "@noverax/courier/define"

const { endpoint } = courier()

const auth = defineGroup({
  prefix: "auth",
  expose: true,
  endpoints: {
    // The backend answers a sign-in with its session cookie
    login: endpoint.post("login").variables<{ email: string; password: string }>().headers(["set-cookie"]),
    // Calls made as the user send the session back
    me: endpoint.get("me").response<{ id: string; name: string }>().headers(["cookie"]),
    // Signing out sends the session and clears it
    logout: endpoint.post("logout").headers(["cookie", "set-cookie"]),
  },
})

export const api = defineApi({ baseURL: process.env.AUTH_API_URL!, groups: { auth } })
```

```ts title="Browser"
const caller = createProxyCaller(apiRoutes)

await caller.auth.login({ email, password }) // the session cookie reaches the browser
const me = await caller.auth.me()            // and goes back to the backend with this call
```

When the whole backend is your own, name both once for every endpoint: `defineApi({ baseURL, allowedHeaders: ["cookie", "set-cookie"], groups })`. The browser sends its cookies to the proxy route because `credentials: "include"` is the proxy client's default.

> **set-cookie handling**
>
> A named `set-cookie` is passed back on every answer, an error included, so a sign-out that fails still clears the session. Courier uses `getSetCookie()` to keep each cookie apart, avoiding the common bug of splitting cookie values on commas.

## Cancelled calls

When the browser cancels a call — an aborted upload, a query it no longer needs — the handler stops its own request to the backend. It learns that the client has gone from the request's `signal`, and not every framework fires it:

| Server | A cancelled call stops upstream |
| --- | --- |
| Express, Fastify, NestJS, the standalone server, Hono, Next.js, `@astrojs/node` | Yes |
| SvelteKit with `@sveltejs/adapter-node` | Yes: the adapter watches the socket from `platform.req` |
| SolidStart, dev and production | Yes: the adapter watches the response on `nativeEvent` |
| React Router 7 | Only with `future: { v8_passThroughRequests: true }`: without it a resource route receives a rebuilt copy of the request whose signal does not reliably follow the original |
| Nuxt, dev and production | Yes: the adapter converts Node's request so its signal follows the connection, and a Web request h3 1.x hands over is watched through the event's Node response |
| `vite dev` for SvelteKit, `astro dev` | No: the dev server exposes neither a signal nor the connection |

An integration of your own that can reach the Node socket or response passes it to `handleWhileConnected`, which gives the handler a request whose signal also fires when that connection closes:

```typescript
import { createHandler, handleWhileConnected } from "@noverax/courier"

const { handle } = createHandler()

// res is the Node response your framework is answering on
const response = await handleWhileConnected(handle, request, res)
```

## Error responses

The handler returns consistent JSON error responses with appropriate HTTP status codes:

```typescript
// Error responses follow a consistent format:

// HTTP errors from the backend:
// Status: forwarded from backend (e.g., 404, 422, 500)
// Body:   parsed JSON from backend response

// Validation errors (missing source, endpoint not found):
// Status: 400
// Body:   { success: false, message: "[api][users][getById]: method not found" }

// Origin mismatch:
// Status: 403
// Body:   { success: false, message: "Origin mismatch" }

// Invalid JSON body:
// Status: 400
// Body:   { success: false, message: "Invalid JSON body" }
```

## Next Steps

- [createProxyCaller](/docs/proxy-mode/proxy-caller) — configure the browser-side client
- [Path Parameters](/docs/advanced/path-params) — dynamic URL segments
- [Encoding](/docs/advanced/encoding) — custom body and query serialization
