The Singleton Pattern
One createProxyQueryCaller call exports typed hooks for the entire app.
// lib/query.ts — Single typed instance for the entire app
import { createProxyQueryCaller } from "@noverax/courier-react"
import { apiRoutes } from "@/courier.routes"
export const q = createProxyQueryCaller(apiRoutes)
// The routes Courier writes from your .expose() marks carry the API's types:
// - The group and endpoint names a page may call
// - Response types and variable types per endpoint
// q.group.useQuery("endpoint") — full autocompletionThe routes Courier writes from the endpoints you mark .expose() carry the API's group, endpoint, response and variable types. Every call site gets full autocompletion and type checking via dot-notation.
Source Definition
Define sources with defineApi and defineGroup functions. Export the sources map and its type.
// api/index.ts — Define and export the API source
import { defineApi } from "@noverax/courier/define"
export const api = defineApi({
baseURL: "http://localhost:4000",
// A page may call every endpoint through the proxy
expose: true,
groups: {
users, // Users group with CRUD endpoints
posts, // Posts group
files, // File upload endpoints
},
})The typeof api type is derived from the actual definitions — no need to manually define or sync types.
Method Types
The builder chain is the foundation of the type system.
// How the builder chain drives the type system
import { courier, defineGroup } from "@noverax/courier/define"
const { endpoint } = courier()
const users = defineGroup({
prefix: "users",
endpoints: {
// .variables() and .response() carry the types forward
getAll: endpoint.get("").response<UserListResponse>(),
getById: endpoint.get(":id").variables<{ id: number }>().response<User>(),
create: endpoint.post("").variables<CreateUserInput>().response<User>(),
update: endpoint.put(":id").variables<{ id: number } & Partial<CreateUserInput>>().response<User>(),
delete: endpoint.del(":id").variables<{ id: number }>().response<{ success: boolean }>(),
},
})
// These types flow through the entire chain:
// endpoint → defineGroup → defineApi → courier.routes.ts → createProxyQueryCaller
// → q.group.useQuery("endpoint") → call siteEach endpoint declares its response and variables types. These flow through the chain: courier → defineGroup → defineApi → courier.routes.ts → createProxyQueryCaller → q.group.useQuery.
Type Inference
Full autocompletion and type checking at every call site — no manual types needed.
// Full type inference at call site — no manual types needed
// useQuery infers Response type from the Endpoint definition
const { data } = q.users.useQuery("getAll")
// ^? { data: User[], total: number } | undefined
// useMutation infers both Response and Variables
const { mutate } = q.users.useMutation("create")
// ^? (variables: { name: string, email: string, role?: string }) => void
// caller (non-hook) also infers types
const users = await caller.users.getAll()
// ^? { data: User[], total: number }
// Dot-notation resolves the correct nested type
const user = await caller.users.getById({ id: 1 })
// ^? UserTypeScript infers group names, endpoint names, response types, and variable types automatically via dot-notation. Errors are caught at compile time.
Multiple Instances
Create separate typed instances for different backends with independent defaults.
// Multiple instances for different backends
import { createProxyQueryCaller } from "@noverax/courier-react"
import { externalApiRoutes, internalApiRoutes } from "@/courier.routes"
// Internal API
export const internalQ = createProxyQueryCaller(internalApiRoutes, {
timeout: 5000,
headers: { "X-Internal": "true" },
})
// External API
export const externalQ = createProxyQueryCaller(externalApiRoutes, {
timeout: 15000,
onError: (error) => Sentry.captureException(error),
})
// Usage — each instance is fully typed independently
const { data } = internalQ.users.useQuery("getAll")
const { mutate } = externalQ.payments.useMutation("charge")Each instance has its own routes, defaults (timeout, headers, error handling), and is fully independent.
Direct Mode
createCaller uses the same type system for server-side usage.
// Direct mode — same type system, no proxy
import { createCaller } from "@noverax/courier"
import { api } from "@/api"
export const caller = createCaller(api)
// Server Component or Server Action
const users = await caller.users.getAll()
// ^? { data: User[], total: number }Direct mode shares the same sources and type inference. Use it in Server Components and Server Actions without the proxy hop.