Import
import { createHandler } from "@noverax/courier"Signature
createHandler takes an optional configuration and returns handlers that work with Web Standard Request/Response.
declare function createHandler(config?: HandlerConfig): AdapterHandlerimport { 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 |
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 |
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
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
import { registerSources } from "@noverax/courier"
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
const { sources } = await import("./api")
registerSources(sources)
}
}import { apiRoute } from "@noverax/courier-next"
export const { GET, POST } = apiRoute()Express
import { registerSources } from "@noverax/courier"
import { courierHandler } from "@noverax/courier-express"
import { sources } from "./api"
registerSources(sources)
app.use("/api/request", courierHandler())Hono
import { registerSources } from "@noverax/courier"
import { courierHandler } from "@noverax/courier-hono"
import { sources } from "./api"
registerSources(sources)
app.all("/api/request/*", courierHandler())Fastify
import { registerSources } from "@noverax/courier"
import { courierPlugin } from "@noverax/courier-fastify"
import { sources } from "./api"
registerSources(sources)
fastify.register(courierPlugin, { prefix: "/api/request" })NestJS
import { registerSources } from "@noverax/courier"
import { courierMiddleware } from "@noverax/courier-nestjs"
import { sources } from "./api"
registerSources(sources)
export class CourierMiddleware {
use = courierMiddleware()
}Nuxt
import { registerSources } from "@noverax/courier"
import { sources } from "~/api"
export default defineNitroPlugin(() => registerSources(sources))import { courierHandler } from "@noverax/courier-nuxt"
export default courierHandler()Request Flow
# 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 answerSecurity
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 for the whole rule.
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 — architecture and security
- createProxyCaller — the client that sends requests to the handler
- Framework Guides — framework-specific setup