# Caller & Proxy

Two transport modes for calling your API — direct and proxy — with the same type-safe interface.

## Overview

Courier provides two ways to call your API endpoints. Both share the same `TypedClient` interface and the same type inference:

| Factory | Mode | Use Case |
|---------|------|----------|
| `createCaller(api)` | Direct | Server-side, Node.js, Bun, Deno, scripts |
| `createProxyCaller(apiRoutes)` | Proxy | Browser → your server → backend |

## createCaller (Direct Mode) (direct mode)

Calls your backend directly. Works in any JavaScript runtime with `fetch`.

```ts title="lib/caller.ts"
import { createCaller } from "@noverax/courier"
import { api } from "@/api/sources"

export const caller = createCaller(api)
```

```ts
// Works anywhere: Node.js, Bun, Deno, Server Components, scripts
const users = await caller.users.getAll()
//    ^? User[]

const user = await caller.users.getById({ id: "1" })
//    ^? User
```

> **When to use direct mode**
>
> Use direct mode for server-side code where you have access to the `api` object: Server Components, Server Actions, API routes, CLI scripts, cron jobs.

## createProxyCaller (Proxy Mode) (proxy mode)

Sends requests through your server's proxy route. The browser never sees your backend URL. The caller takes the routes Courier writes into `courier.routes.ts` from the endpoints you marked `.expose()`: what a page may call, and how it reaches each — see [Exposing Endpoints](/docs/proxy-mode/expose).

```ts title="lib/caller.ts"
import { createProxyCaller } from "@noverax/courier"
import { apiRoutes } from "@/courier.routes"

export const caller = createProxyCaller(apiRoutes, {
  proxyPath: { http: "/api/request" },  // optional, defaults to /api/request
  timeout: 10000,
  credentials: "include",               // send the browser's cookies to the proxy route
  headers: { "X-Custom": "value" },
  onError: (error) => console.error(error),
  onSuccess: (data) => console.log(data),
})
```

```ts
// Browser sends POST /api/request → your server → backend
// (a read that declares .cache() goes as GET)
const users = await caller.users.getAll()
```

> **The routes, not the API**
>
> Pass the routes, never the `api` object: a page that imports the definition as a value ships it — the upstream host, the middleware, every endpoint path. The routes carry the API's type and each endpoint's transport, which is all the caller needs.

### ProxyCallerOptions

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `proxyPath` | `ProxyPath` | per-transport defaults | Per-transport proxy endpoint paths (`http` / `sse` / `ws`) |
| `timeout` | `number` | — | Request timeout in ms |
| `cancelPrevious` | `readonly string[]` | the API's own | The reads a newer call cancels while in flight |
| `coalesce` | `boolean` | the API's own, or `true` | Whether identical reads in flight share one request |
| `credentials` | `RequestCredentials` | `"include"` | Whether the browser sends its cookies to the proxy route; the proxy forwards them where the source or endpoint names `cookie` |
| `headers` | `Record<string, string>` | — | Default headers |
| `onError` | `(error: HTTPError) => void` | — | Global error callback |
| `onSuccess` | `(data: unknown) => void` | — | Global success callback |

`ProxyPath` shape:

| Field | Default | Notes |
|---|---|---|
| `proxyPath.http` | `"/api/request"` | HTTP proxy endpoint |
| `proxyPath.sse` | `"/api/sse"` | SSE proxy endpoint |
| `proxyPath.ws` | **none** — required for WS endpoints | Use absolute `wss://` URL for Next.js / serverless. See [standalone server](/docs/deployment/standalone-server). |

## The Dot-Notation Interface

Both factories return a typed client with dot-notation access:

```ts
const caller = createCaller(api)

// Access endpoints via group.method notation
const users = await caller.users.getAll()
const user = await caller.users.getById({ id: "1" })
```

### Call Options

Per-call options are named with `.with()`, which returns a caller carrying them:

```ts
// Stream mode — the body arrives as a ReadableStream instead of parsed
const response = await caller.events.with({ stream: true }).subscribe({ channel: "builds" })

// A read that may be cached goes to the proxy as GET; POST keeps this call's variables out of the address
const users = await caller.users.with({ method: "POST" }).getAll({ page: 1 })

// Upload progress tracking
await caller.files
  .with({
    onUploadProgress: ({ percent }) => console.log(`${Math.round(percent * 100)}%`),
  })
  .upload(formData)

// Abort signal
const controller = new AbortController()
await caller.users.with({ signal: controller.signal }).getAll()
```

## Using Both Modes Together

Most apps use both modes — proxy for the browser, direct for the server. Keep them in separate modules: the server's caller holds the API definition, and a browser module that imported it would ship the definition with the page.

```ts title="lib/server-caller.ts"
import { createCaller } from "@noverax/courier"
import { api } from "@/api/sources"

// Server-side (RSC, Server Actions, API routes)
export const serverCaller = createCaller(api)
```

```ts title="lib/client-caller.ts"
import { createProxyCaller } from "@noverax/courier"
import { apiRoutes } from "@/courier.routes"

// Client-side (React components): the routes Courier writes, never the API itself
export const clientCaller = createProxyCaller(apiRoutes)
```

See [Exposing Endpoints](/docs/proxy-mode/expose) for what the routes name and why they keep the definition out of the bundle.

## Next Steps

- [Framework Guides](/docs/frameworks/nextjs) — complete setup for your framework
- [React Query](/docs/react-query/setup) — hooks with full type inference
- [Error Handling](/docs/core-concepts/error-handling) — handling HTTPError
