In one command
Run it in your project — Next.js, Nuxt, SvelteKit, Astro, React Router, SolidStart, React or Vue with Vite, Expo, Express, Hono, Fastify or NestJS:
npx @noverax/create-courierIt shows the plan, and once you agree adds a typed example API, the route the browser calls it through, courier.config.ts, a mock and a page at /courier; installs the packages with your package manager; and type-checks what it added. Then:
npm run studio # Courier Studio, and the mock API it runs
npm run dev # your app: open /courierThe page lists the users your API answers with — from the mock until API_URL names your backend — and creates one from a form; every call shows in Studio. For a new project, name a folder and the framework: npx @noverax/create-courier shop --framework next. Create Courier has every option, and what it changes in each framework.
By hand
The same setup, step by step.
Step 1: Define your API
Create a file with your API definition using courier, defineGroup, and defineApi:
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>(),
update: endpoint.put(":id").variables<{ id: string; name: string }>().response<User>(),
remove: endpoint.del(":id").variables<{ id: string }>().response<null>(),
},
})
export const api = defineApi({
baseURL: process.env.API_URL!,
groups: { users },
})Path parameters
The :id syntax automatically extracts id from variables and inserts it into the URL. Remaining variables become query params (GET) or body (POST/PUT/PATCH).
Step 2: Set up the caller
Choose the mode that fits your use case:
Direct mode — call your backend from any runtime:
import { createCaller } from "@noverax/courier"
import { api } from "@/api/sources"
export const caller = createCaller(api)Proxy mode — browser requests go through your server (hides API keys, bypasses CORS). A boundary names the endpoints the browser may call:
import { defineBoundary } from "@noverax/courier/define"
export const boundary = defineBoundary({
source: "api",
expose: { "users.getAll": "query", "users.getById": "query", "users.create": "mutation" },
})The API holds it, so the server refuses every other endpoint — add boundary to the defineApi call in api/sources.ts, imported from ./boundary. Then the browser's caller takes it in place of the API:
import { createProxyCaller } from "@noverax/courier"
import type { api } from "@/api/sources"
import { boundary } from "@/api/boundary"
export const caller = createProxyCaller<typeof api>("api", { boundary })The caller sends each call to /api/request on your server, which runs it with the sources registered there — your framework guide sets up that route and the registration. See defineBoundary for what the browser learns and what it never does.
React Query — hooks with full type inference:
import { createProxyQueryCaller } from "@noverax/courier-react"
import type { api } from "@/api/sources"
export const q = createProxyQueryCaller<typeof api>("api")Use both modes together
Most apps use proxy/React Query for client-side and direct mode for server-side. Both share the same API definitions.
Step 3: Use it
With React Query:
"use client"
import { q } from "@/lib/query"
export default function UsersPage() {
const { data: users, isLoading } = q.users.useQuery("getAll")
// ^? User[]
const { mutate } = q.users.useMutation("create", {
onSuccess: (user) => console.log("Created:", user.name),
// ^? string
})
if (isLoading) return <p>Loading...</p>
return (
<div>
<ul>
{users?.map((u) => <li key={u.id}>{u.name} — {u.email}</li>)}
</ul>
<button onClick={() => mutate({ name: "Jane", email: "jane@test.com" })}>
Add User
</button>
</div>
)
}Direct mode (server-side):
import { caller } from "@/lib/caller"
export default async function DashboardPage() {
const users = await caller.users.getAll()
// ^? User[]
return <ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>
}Vanilla TypeScript (no framework):
import { createCaller } from "@noverax/courier"
import { api } from "./api/sources"
const caller = createCaller(api)
const users = await caller.users.getAll()
const user = await caller.users.getById({ id: "1" })What's happening under the hood
When you call caller.users.getById({ id: "5" }):
- Courier resolves the
usersgroup andgetByIdmethod - The
:idpath parameter is extracted from variables →GET /users/5 - Middleware chain runs (retry, auth, logging, etc.)
- Schema validation runs (if configured)
- The typed response flows back to your code
In proxy mode, step 2-5 happen on your server. The browser only sees /api/request.
Next steps
- Framework Guides — complete setup for your framework
- Middleware — add retry, auth, logging
- Examples — interactive demos