Skip to content

Getting StartedProject Setup

Project Setup

Recommended file structure, patterns, and configuration for production apps.

2 min read

Keep your API definitions separate from your UI code. The structure is the same regardless of framework:

Project structure
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.ts

Source definitions

The api/ directory contains your API structure. Each group lives in its own file:

api/users.ts
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:

api/index.ts
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:

lib/query.ts (React Query)
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")
lib/caller.ts (Vanilla / Vue / Svelte)
import { createProxyCaller } from "@noverax/courier"
import type { api } from "@/api"
import { boundary } from "@/api/boundary"

export const caller = createProxyCaller<typeof api>("api", { boundary })
lib/server-caller.ts (Server-side)
import { createCaller } from "@noverax/courier"
import { api } from "@/api"

export const caller = createCaller(api)

Multiple backends

Define a separate defineApi for each backend service:

api/index.ts
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:

ts
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:

.env.local
API_URL=http://localhost:4000
AUTH_API_URL=http://localhost:5000

Never 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