Skip to content

API ReferencecreateHandler

createHandler

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

3 min readproxy mode

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

OptionDefaultDescription
allowedOriginsown origin; local ports in development, for an app served locallyOrigins of other pages that may call the proxy — see Origin Validation
allowedHostsloopback names and IP literals; every name in productionHost names the app is reached by; a request for any other gets 421 — see Host validation
maxBodyBytes1 MBLargest JSON envelope the handler reads; a larger one is refused with 413
maxUploadBytes10 MBLargest multipart/form-data upload the handler reads; a larger one is refused with 413
sseHeaders—Extra response headers on every SSE stream
sseMaxLifetimeMs30 minutesLongest an SSE stream stays open before the client reconnects with Last-Event-ID
maxSSEConnectionsunlimitedMost 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

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

export async function register() {
  if (process.env.NEXT_RUNTIME === "nodejs") {
    const { sources } = await import("./api")
    registerSources(sources)
  }
}
app/api/request/route.ts
import { apiRoute } from "@noverax/courier-next"

export const { GET, POST } = apiRoute()

Express

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

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

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

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

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

export default defineNitroPlugin(() => registerSources(sources))
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

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.

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.