# createHandler

Universal adapter factory that creates Web Standard handlers for proxy-mode requests from any framework.

## Import

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

## Signature

`createHandler` takes an optional configuration and returns handlers that work with Web Standard `Request`/`Response`.

```ts
declare function createHandler(config?: HandlerConfig): AdapterHandler
```

```ts
import { sources } from "@/api"

const { handle, handleGet, handlePost } = createHandler({ sources })
```

## Parameters

### config.sources

A record of API source definitions created with `defineApi()`. Given, the handler answers for those sources alone, and a later `registerSources()` in the same process does not change what it serves. Left out, the handler reads the process-wide registry that `registerSources()` and `attachRuntime()` fill, at request time — which is how every framework adapter works.

### Other options

| Option | Default | Description |
|--------|---------|-------------|
| `allowedOrigins` | own origin; local ports in development, for an app served locally | Origins of other pages that may call the proxy — see [Origin Validation](#origin-validation) |
| `allowedHosts` | loopback names and IP literals; every name in production | Host names the app is reached by; a request for any other gets `421` — see [Host validation](/docs/proxy-mode/overview#host-validation) |
| `maxBodyBytes` | 1 MB | Largest JSON envelope the handler reads; a larger one is refused with 413 |
| `maxUploadBytes` | 10 MB | Largest `multipart/form-data` upload the handler reads; a larger one is refused with 413 |
| `sseHeaders` | — | Extra response headers on every SSE stream |
| `sseMaxLifetimeMs` | 30 minutes | Longest an SSE stream stays open before the client reconnects with `Last-Event-ID` |
| `maxSSEConnections` | unlimited | Most SSE streams open at once |
| `onSSEConnect`, `onSSEDisconnect`, `onSSEError` | — | SSE lifecycle hooks |

Every option is optional, so `createHandler()` is a complete call.

## Return Type

```ts
type AdapterHandler = {
  /** Routes GET, POST and SSE; answers any other method with 405 */
  handle: (request: Request) => Promise<Response>
  handleGet: (request: Request) => Promise<Response>
  handlePost: (request: Request) => Promise<Response>
  handleSSE: (request: Request) => Promise<Response>
  /** The SSE streams open right now */
  readonly sseConnections: SSEConnectionRegistry
  /** Tell every SSE client to reconnect later, then close the streams */
  shutdownSSE: (options?: { retryMs?: number; drainMs?: number }) => Promise<void>
}
```

## Framework Adapters

Each framework has a dedicated adapter package that wraps `createHandler()`. Every adapter is **zero-arg** — sources are read at request time from the core global registry populated by `registerSources()` (manual) or `attachRuntime()` (auto-loads `courier.config.ts`).

### Next.js

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

export async function register() {
  if (process.env.NEXT_RUNTIME === "nodejs") {
    const { sources } = await import("./api")
    registerSources(sources)
  }
}
```

```ts title="app/api/request/route.ts"
import { apiRoute } from "@noverax/courier-next"

export const { GET, POST } = apiRoute()
```

### Express

```ts title="server.ts"
import { registerSources } from "@noverax/courier"
import { courierHandler } from "@noverax/courier-express"
import { sources } from "./api"

registerSources(sources)
app.use("/api/request", courierHandler())
```

### Hono

```ts title="server.ts"
import { registerSources } from "@noverax/courier"
import { courierHandler } from "@noverax/courier-hono"
import { sources } from "./api"

registerSources(sources)
app.all("/api/request/*", courierHandler())
```

### Fastify

```ts title="server.ts"
import { registerSources } from "@noverax/courier"
import { courierPlugin } from "@noverax/courier-fastify"
import { sources } from "./api"

registerSources(sources)
fastify.register(courierPlugin, { prefix: "/api/request" })
```

### NestJS

```ts title="courier.middleware.ts"
import { registerSources } from "@noverax/courier"
import { courierMiddleware } from "@noverax/courier-nestjs"
import { sources } from "./api"

registerSources(sources)

export class CourierMiddleware {
  use = courierMiddleware()
}
```

### Nuxt

```ts title="server/plugins/courier.ts"
import { registerSources } from "@noverax/courier"
import { sources } from "~/api"

export default defineNitroPlugin(() => registerSources(sources))
```

```ts title="server/api/request/[...].ts"
import { courierHandler } from "@noverax/courier-nuxt"

export default courierHandler()
```

## Request Flow

```bash
# Every request:
# 1. Refuse a host the app is not reached by (421)
# 2. Answer a preflight; refuse another site's page (403)

# POST request flow:
# 3. Read the endpoint from the URL: _s (source), _g (group), _m (method)
#    — the call is known, and shown in Studio, before its body arrives
# 4. Read the body within its limit (413): { variables, options } as JSON,
#    or the user's form behind an _options field
# 5. Call executeRequest(sources, payload, context) and return its answer

# GET request flow:
# 3. Read the endpoint, _v (variables, JSON) and _o (options) from the URL
# 4. Call executeRequest(sources, payload, context) and return its answer
```

## Security

### Marked Endpoints

The handler serves an endpoint only when a mark opens it — `.expose()` on the endpoint, or `expose` on its group or its API, the nearest deciding — and answers any other exactly as a name the source does not define, `400`, so a caller guessing names learns nothing of what exists. See [Exposing Endpoints](/docs/proxy-mode/expose).

### Origin Validation

The handler refuses a request another site's page made with `403 { success: false, message: "Origin not allowed" }`. The browser's `Sec-Fetch-Site` decides where it is sent; otherwise the `Origin` must be the origin the request was addressed to. Pages on other origins are served when `allowedOrigins` lists them — see [Proxy Mode](/docs/proxy-mode/overview#origin-validation) for the whole rule.

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

### Header Whitelisting

Only safe headers are forwarded from the browser to the backend. Extend the whitelist with `allowedHeaders` in your `defineApi()` options.

> **Web Standard**
>
> `createHandler` works with any runtime that supports the Web Standard `Request`/`Response` API — Node.js, Deno, Bun, Cloudflare Workers, and more. Framework adapters handle the mapping from framework-specific types.

## Related

- [Proxy Mode](/docs/proxy-mode/overview) — architecture and security
- [createProxyCaller](/docs/api/create-proxy-caller) — the client that sends requests to the handler
- [Framework Guides](/docs/frameworks/nextjs) — framework-specific setup
