Courier is built around a simple idea: define your API once, call it from anywhere. The architecture has four layers — definition, transport, execution, and integration — that work together across any JavaScript framework.
The Four Layers
// Layer 1 — Definition (shared across all frameworks)
const api = defineApi({ baseURL: "...", groups: { users } })
export { api }
// Layer 2 — Transport (mode-specific)
// Direct: createCaller(api) → executeRequest → fetch → backend
// Proxy: createProxyCaller<typeof api>("api") → GET or POST /api/request → adapter → executeRequest → fetch → backend
// Layer 3 — Execution (shared core engine)
// executeRequest → middleware chain → schema validation → HttpClient → fetch
// Layer 4 — Integration (optional)
// createProxyQueryCaller wraps createProxyCaller with React Query hooks- Definition —
courier,defineGroup,defineApidefine your API structure, types, and validation. Shared between all modes and frameworks. - Transport — determines how the request reaches the backend. Direct calls it immediately; proxy goes through your server.
- Execution —
executeRequestis the single orchestrator that runs middleware, validates schemas, and delegates to the HTTP client. - Integration — optional wrappers like
createProxyQueryCallerthat add React Query hooks on top.
Request Flow
Direct Mode
Server-side code calls the backend with no proxy hop:
┌──────────────────────────────────────────────────────────────┐
│ Any Runtime (Node.js, Bun, Deno, RSC, Server Actions) │
│ │
│ createCaller(api) │
│ │ │
│ ▼ │
│ executeRequest(sources, payload, context) │
│ │ → middleware chain │
│ │ → schema validation │
│ ▼ │
│ HttpClient → native fetch → GET/POST/PUT/PATCH/DELETE │
└──────────────────┬───────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ Your Backend (Express, FastAPI, Rails, Go, Laravel...) │
└──────────────────────────────────────────────────────────────┘Proxy Mode
The browser sends requests through your server, which hides API keys and bypasses CORS:
┌──────────────────────────────────────────────────────────────┐
│ Browser │
│ │
│ createProxyCaller<typeof api>("api") │
│ │ │
│ ▼ │
│ fetch("/api/request?_s=api&_g=users&_m=create", { │
│ method: "POST", │
│ body: { variables, options } │
│ }) │
└──────────────────┬───────────────────────────────────────────┘
│ POST /api/request (a read goes as GET, its variables in _v)
▼
┌──────────────────────────────────────────────────────────────┐
│ Your Server (Next.js, Nuxt, Hono, Express, Fastify...) │
│ │
│ Server Adapter → createHandler({ sources }) │
│ │ → origin validation │
│ │ → cookies only where named │
│ ▼ │
│ executeRequest(sources, payload, context) │
│ │ → middleware chain │
│ │ → schema validation │
│ ▼ │
│ HttpClient → native fetch → GET/POST/PUT/PATCH/DELETE │
└──────────────────┬───────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ Your Backend (any REST API) │
└──────────────────────────────────────────────────────────────┘Security built in
Proxy mode validates the Origin header, whitelists forwarded headers, and keeps your backend URL and API keys server-side. The browser only sees /api/request.
Core Components
defineApi / defineGroup / endpoint
The functional API for defining your sources. No classes, no inheritance — just plain functions that produce typed objects:
import { courier, defineApi, defineGroup } from "@noverax/courier/define"
const { endpoint } = courier()
type User = { id: string; name: string; email: string }
const users = defineGroup({
prefix: "users",
endpoints: {
getAll: endpoint.get("").response<User[]>(),
getById: endpoint.get(":id").variables<{ id: string }>().response<User>(),
create: endpoint.post("").variables<{ name: string; email: string }>().response<User>(),
},
})
const api = defineApi({
baseURL: "https://api.example.com",
groups: { users },
})
export { api }executeRequest
The core engine. Both modes converge here. It resolves the source, group, and method, runs the middleware chain, validates schemas, and delegates to the HTTP client:
// executeRequest is the single core orchestrator
executeRequest(sources, {
source: "api",
group: "users",
method: "getById",
variables: { id: "5" },
}, context)
// Internally:
// 1. Look up sources["api"] → api definition
// 2. Find the group: api.groups["users"]
// 3. Run middleware chain (retry, auth, logging...)
// 4. Validate variables against schema (if configured)
// 5. Create HttpClient with baseURL + prefix
// 6. Call the endpoint: users.getById(http, vars)
// 7. Validate response against schema (if configured)
// 8. Return the typed responseHttpClient
A thin wrapper around native fetch. Zero dependencies. Handles URL params, body encoding, search params, and abort controllers:
// HttpClient generates typed HTTP methods from baseURL + prefix
// Each method resolves :param placeholders from variables:
// get(":id", { id: "5" }) → GET /users/5
// post("", vars) → POST /users with JSON bodycreateHandler
The universal adapter interface. All server framework adapters use it internally:
// Core provides createHandler() → { handle, handleGet, handlePost }
// Sources come from the core global registry populated by
// `registerSources()` or `attachRuntime()`. Each adapter wraps
// `createHandler()` for its framework:
// Next.js: apiRoute() → { GET, POST }
// Hono: courierHandler() → (c) => Response
// Express: courierHandler() → (req, res) => void
// Fastify: courierPlugin → FastifyPluginAsync
// NestJS: courierMiddleware() → (req, res, next) => void
// Nuxt: courierHandler() → H3EventHandlerFactory Functions
| Factory | Mode | Environment | Returns |
|---|---|---|---|
createCaller(api) | Direct | Any runtime | TypedClient<typeof api> |
createProxyCaller<typeof api>("api") | Proxy | Browser | TypedClient<typeof api> |
createQueryCaller(api) | Direct + hooks | Browser (React) | QueryCallerClient<typeof api> |
createProxyQueryCaller<typeof api>("api", { boundary }) | Proxy + hooks | Browser (React) | QueryCallerClient<typeof api> |
The callers return a TypedClient<typeof api>, and the query callers the same groups with React Query hooks on each — q.users.useQuery("getAll") — so type inference is identical whichever factory you use. Build each once in a module of its own: see Singleton Pattern.
Next Steps
- Defining APIs — deep dive into defineApi, defineGroup, endpoint
- Framework Guides — complete setup for your framework
- Middleware — composable middleware chain