Recommended file structure
Keep your API definitions separate from your UI code. The structure is the same regardless of framework:
src/
├── api/
│ ├── index.ts # defineApi + sources export
│ ├── users.ts # users group (defineGroup)
│ ├── posts.ts # posts group
│ └── files.ts # files group
├── lib/
│ ├── caller.ts # createProxyCaller, built once
│ ├── query.ts # createProxyQueryCaller, built once (React)
│ └── server-caller.ts # createCaller for server code
├── app/ # (Next.js) or pages/ (Nuxt) or routes/ (SvelteKit)
│ ├── api/
│ │ └── request/
│ │ └── route.ts # proxy route handler
│ └── ...
└── server/ # (if using Express/Hono/Fastify separately)
└── index.tsSource definitions
The api/ directory contains your API structure. Each group lives in its own file:
import { courier, defineGroup } from "@noverax/courier/define"
const { endpoint } = courier()
type User = { id: string; name: string; email: string }
export 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>(),
},
})The main file combines groups into an API source:
import { defineApi } from "@noverax/courier/define"
import { users } from "./users"
import { posts } from "./posts"
import { files } from "./files"
const api = defineApi({
baseURL: process.env.API_URL!,
groups: { users, posts, files },
})
export { api }One group per file
Keeping each group in its own file makes it easy to find and modify endpoints. Types stay co-located with the endpoints that use them.
Caller setup
Create the appropriate caller for your use case, once, in a module of its own, and import it wherever a call is made — see Singleton Pattern:
import { createProxyQueryCaller } from "@noverax/courier-react"
import type { api } from "@/api"
import { boundary } from "@/api/boundary"
export const q = createProxyQueryCaller<typeof api>("api", { boundary })
// In components: q.users.useQuery("getAll"), q.users.useMutation("create")import { createProxyCaller } from "@noverax/courier"
import type { api } from "@/api"
import { boundary } from "@/api/boundary"
export const caller = createProxyCaller<typeof api>("api", { boundary })import { createCaller } from "@noverax/courier"
import { api } from "@/api"
export const caller = createCaller(api)Multiple backends
Define a separate defineApi for each backend service:
import { defineApi } from "@noverax/courier/define"
import { users, posts } from "./groups"
import { auth } from "./auth"
const mainApi = defineApi({
baseURL: process.env.API_URL!,
groups: { users, posts },
})
const authApi = defineApi({
baseURL: process.env.AUTH_API_URL!,
groups: { auth },
})
export { mainApi, authApi }Create separate callers for each API, with full type inference:
const mainCaller = createCaller(mainApi)
const authCaller = createCaller(authApi)
const users = await mainCaller.users.getAll()
const token = await authCaller.auth.login({ email, password })Environment variables
Backend URLs should always be server-side environment variables:
API_URL=http://localhost:4000
AUTH_API_URL=http://localhost:5000Never expose API_URL to the client
In proxy mode, the browser only talks to /api/request. Your backend URL, API keys, and other secrets stay on the server.
Next steps
- Framework Guides — complete setup for your framework
- Examples — interactive demos of every feature