Skip to content

ExamplesTyped Singleton

Typed Singleton

Create a single typed request instance with full type inference across your entire application.

both modes

The Singleton Pattern

One createProxyQueryCaller call exports typed hooks for the entire app.

ts
// 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 autocompletion

The 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.

Singleton docs

Source Definition

Define sources with defineApi and defineGroup functions. Export the sources map and its type.

ts
// 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.

ts
// 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 site

Each 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.

ts
// 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 })
//    ^? User

TypeScript 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.

ts
// 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.

ts
// 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.