The @noverax/courier-express adapter provides a handler for Express that serves as the Courier proxy route.
In one command
npx @noverax/create-courierIn an Express server it adds a router with the proxy route and a page at /courier, mounted on your app before its body parsers, with a typed example API and its mock. See Create Courier.
Installation
npm install @noverax/courier @noverax/courier-express express cors
npm install -D @types/express @types/corsDefine your API sources
import { courier, defineApi, defineGroup } from "@noverax/courier/define"
const { endpoint } = courier()
type User = { id: number; name: string; email: string }
const users = defineGroup({
prefix: "users",
endpoints: {
getAll: endpoint.get("").response<User[]>(),
getById: endpoint.get(":id").variables<{ id: number }>().response<User>(),
create: endpoint.post("").variables<{ 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 }Setup
Create an Express server with the Courier handler:
import express from "express"
import cors from "cors"
import { registerSources } from "@noverax/courier"
import { courierHandler } from "@noverax/courier-express"
import { sources } from "./lib/sources"
registerSources(sources)
const app = express()
app.use(cors({ origin: "http://localhost:5173", credentials: true }))
// No body parser before this line: Courier reads the raw body itself, and a
// global `express.json()` would consume it first.
// Mount the Courier proxy route
app.all("/api/request", courierHandler())
app.listen(3001, () => {
console.log("Server running on http://localhost:3001")
})registerSources(sources) populates the in-process registry that courierHandler() reads at request time. The adapter is zero-arg — pass only behavioral options like allowedOrigins or sseHeaders. When you wire Studio on top (see below), attachRuntime() also loads courier.config.ts and registers its sources in development; keep your own registerSources() call, which is what registers them in production, where the runtime is not attached.
With existing Express app
If you already have an Express server, add the Courier handler alongside your existing routes:
import express from "express"
import cors from "cors"
import { registerSources } from "@noverax/courier"
import { courierHandler } from "@noverax/courier-express"
import { sources } from "./lib/sources"
registerSources(sources)
const app = express()
app.use(cors({ origin: "http://localhost:5173", credentials: true }))
// Courier reads the raw body itself, so the proxy route must come before any
// body parser — or the parser must be scoped to the routes that need it.
app.all("/api/request", courierHandler())
// Your existing routes, with body parsing where you want it
app.use(express.json())
app.get("/health", (req, res) => {
res.json({ status: "ok" })
})
app.get("/api/stats", (req, res) => {
res.json({ uptime: process.uptime() })
})
app.listen(3001)Body parsing
A body parser mounted with a bare app.use(express.json()) applies to every route, /api/request included. The Courier handler reads the raw request body itself, so a parser that ran first leaves it nothing — the request fails with REQUEST_BODY_ALREADY_READ, which names this as the cause.
Either register the proxy route before the parser, as above, or scope the parser to the paths that need it: app.use("/api/users", express.json()). The same applies to express.urlencoded() and multer.
CORS configuration
For production, restrict the allowed origins:
import express from "express"
import cors from "cors"
import { registerSources } from "@noverax/courier"
import { courierHandler } from "@noverax/courier-express"
import { sources } from "./lib/sources"
registerSources(sources)
const app = express()
app.use(
cors({
origin: process.env.ALLOWED_ORIGINS?.split(",") ?? "http://localhost:5173",
credentials: true,
})
)
// Before any body parser — Courier reads the raw body itself.
app.all("/api/request", courierHandler())
app.listen(Number(process.env.PORT) || 3001)Credentials
Set credentials: true in your CORS config if your backend uses cookies for authentication. The Courier proxy route sends your backend the session its source describes, and hands the browser the session cookies the backend sets — see User Sessions.
The user's session
An API that keeps a session gets the user's session with every call a route, a service or a step it awaits makes, once the request is served with it at hand:
import { requestContext } from "@noverax/courier-express"
app.use(requestContext())Mount it before the routes that call APIs. The session cookies the API sets reach the browser on the response, and the user's address follows the rule the proxy route follows. See User Sessions.
Studio integration & appOrigin
Wire your Express server into Studio with a single attachRuntime() call. In development it loads courier.config.ts from disk, registers its sources, and opens the connection to Studio; in production the app's own registerSources() call registers them. The appOrigin field for Playground proxy SSE/WS resolution lives in courier.config.ts.runtime.appOrigin (or set the COURIER_APP_URL env var to override per-environment).
import express from "express"
import { courierHandler } from "@noverax/courier-express"
if (process.env.NODE_ENV !== "production") {
const { attachRuntime } = await import("@noverax/courier-studio-core/runtime")
await attachRuntime()
}
const app = express()
app.all("/api/request", courierHandler())
app.listen(3001)Alternatively, set COURIER_APP_URL instead of pinning appOrigin in the config:
COURIER_APP_URL=http://localhost:3001 node server.jsNo magic default
There is no localhost:3001 auto-fallback. If appOrigin is missing, clicking "Subscribe" on an SSE/WS endpoint in Studio Playground proxy mode throws ConfigurationError("APP_ORIGIN_NOT_CONFIGURED") with a pointer back to this section. The correct value depends on your runtime — custom port, Docker host, HTTPS, reverse proxy — and a wrong guess would silently route requests to nowhere.
Client-side setup
On the client, point your caller to this server's proxy URL. Choose the guide for your frontend:
- React (Vite / CRA) —
createProxyQueryCallerwith React Query hooks - Vue —
createProxyCallerwith composables - Next.js — full-stack with
apiRouteandcreateProxyQueryCaller - Nuxt — full-stack with
courierHandlerandcreateProxyCaller
import { createProxyCaller } from "@noverax/courier"
import type { api } from "../shared/sources"
export const caller = createProxyCaller<typeof api>("api", {
proxyPath: { http: "http://localhost:3001/api/request" },
})