# createProxyCaller

The browser-side factory that sends typed requests through your server proxy endpoint.

## Overview

`createProxyCaller` creates a typed client with dot-notation access that calls your server's proxy routes. It is the way a client component calls an API in proxy mode.

It takes the routes of an API: the endpoints a page may call and how the page reaches each, which Courier writes into `courier.routes.ts` from the endpoints you marked `.expose()` — see [Exposing Endpoints](/docs/proxy-mode/expose).

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

// createProxyCaller(routes, options?) returns a client typed from the API
// the routes were written from, with the endpoints they name alone.
export const caller = createProxyCaller(apiRoutes)

// Dot-notation usage:
const result = await caller.users.getById({ id: "5" })
//                         ^? group ^? endpoint  ^? variables
```

The routes name the source as your server registered it, and how each endpoint is reached: a read that may be cached as GET, every other call as POST, a stream over server-sent events and a channel over a socket. Nothing of the API's definition comes with them, so none of it reaches the page's bundle.

## Options

Pass a `ProxyCallerOptions` object to configure every request the client makes:

```typescript
type ProxyPath = {
  // HTTP proxy endpoint — default: "/api/request"
  http?: string
  // SSE proxy endpoint — default: "/api/sse"
  sse?: string
  // WS proxy endpoint — NO default. Required for WS endpoints; use absolute
  // wss:// URL on Next.js / serverless platforms.
  ws?: string
}

type ProxyCallerOptions = {
  // Where the app serves its proxy routes, per transport
  proxyPath?: ProxyPath

  // How long the browser waits for the proxy, in milliseconds
  timeout?: number

  // Cookie behavior — default: "include"
  credentials?: RequestCredentials

  // Extra headers sent with every proxy request
  headers?: Record<string, string>

  // The reads a newer call cancels while they are in flight — default: the API's own
  cancelPrevious?: readonly string[]

  // Whether identical reads in flight share one request — default: the API's own, or true
  coalesce?: boolean

  // Called on every failed request
  onError?: (error: HTTPError) => void | Promise<void>

  // Called on every successful request
  onSuccess?: (data: unknown) => void | Promise<void>

  // How the sockets of channels and push subscriptions behave
  ws?: CallerWSOptions
}
```

## Complete setup

A typical setup creates a single caller instance and exports it for use across all client components:

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

export const caller = createProxyCaller(apiRoutes, {
  timeout: 30_000,
  credentials: "include",
  headers: {
    "x-app-version": "1.0.0",
  },
  onError: (error) => {
    if (error.status === 401) {
      window.location.href = "/login"
    }
  },
})
```

> **One instance per app**
>
> Create `createProxyCaller` once in a shared module. All client components import from the same file, ensuring consistent defaults and callbacks.

## Dot-notation usage

Access endpoints using `caller.{group}.{method}(vars?)` dot notation:

```ts title="Usage examples"
// caller.groupName.endpointName(variables?)
await caller.users.getAll()
await caller.users.getById({ id: 5 })
await caller.posts.create({ title: "Hello", body: "World" })
```

An endpoint the routes do not name is not on the client's type. Called anyway — from JavaScript, or past a cast — the client throws a `ConfigurationError` with the code `ENDPOINT_NOT_IN_ROUTES` before anything is sent, since the proxy would refuse it.

## Raw data access

To access response status and headers, use the `.raw` namespace:

```ts title="Raw data calls"
// Raw data — returns { status, headers, data }
const response = await caller.users.raw.getById({ id: "5" })
```

## Body composition

The endpoint is always named in the URL — `_s` for the source, `_g` for the group, `_m` for the method — so the server knows which call a request is before its body arrives. A group inside another is named by its path: `client.billing.invoices.list()` sends `_g=billing.invoices`. A read the routes say may be cached goes as GET, its variables as JSON in `_v`; every other call goes as POST, and how its body is built depends on the type of variables you pass:

```ts title="JSON vs FormData"
// JSON body (default for objects)
// Sends: POST /api/request?_s=api&_g=users&_m=create
// Body:  { "variables": { "name": "Jane" }, "options": {} }
await caller.users.create({ name: "Jane" })

// FormData body (when variables is FormData)
// Sends: POST /api/request?_s=api&_g=users&_m=create (multipart/form-data)
// Body:  _options={...}, name=Jane, avatar=File
const formData = new FormData()
formData.append("name", "Jane")
formData.append("avatar", fileInput.files[0])
await caller.users.create(formData)
```

> **The _options field**
>
> When sending FormData, Courier writes the call's options as an `_options` field before your own fields. The server handler reads that first `_options` field and passes everything else to your endpoint, so a field of your own named `_method`, `_source` or even `_options` reaches your backend unchanged — a Laravel or Rails form's `_method=PUT` included.

> **Binary variables**
>
> JSON cannot carry a `Blob`, an `ArrayBuffer` or a typed array, so a proxy call whose plain-object variables hold one is refused with a `ConfigurationError` (`PROXY_VARIABLE_NOT_SERIALIZABLE`) before anything is sent. Put binary data in a FormData.

## Binary answers

A source whose `responseType` is `"arrayBuffer"`, `"blob"` or `"formData"` resolves to the same kind of value through the proxy as in direct mode. The proxy sends the bytes as they are, under a content type of its own — `application/vnd.courier.bytes` or `application/vnd.courier.blob`, with `nosniff` — so a browser never renders them as a page of your origin, and the client reads them back; a Blob keeps the upstream's type. The content type is the one header a page reads on a cross-origin answer without it being exposed, so this works for a proxy on another origin too. With `.raw`, the headers are those of the proxy's answer.

## Per-request options

`.with()` returns a caller carrying options for the calls made through it, overriding the defaults for a specific request:

```ts title="Per-request options"
const user = await caller.users
  .with({
    timeout: 5000,
    signal: abortController.signal,
    cache: "no-store",
  })
  .getById({ id: 5 })
```

## Global callbacks

The `onError` and `onSuccess` callbacks run for every request made by this factory. Use them for cross-cutting concerns like authentication redirects or logging:

```ts title="Global error handling"
import { createProxyCaller } from "@noverax/courier"
import { apiRoutes } from "@/courier.routes"

const caller = createProxyCaller(apiRoutes, {
  onSuccess: (data) => {
    // Called after every successful response
    console.log("Response:", data)
  },
  onError: (error) => {
    // Called after every failed response
    // error.status  — HTTP status code
    // error.data    — parsed response body
    // error.headers — response headers

    if (error.status === 401) {
      window.location.href = "/login"
    }

    if (error.status === 429) {
      toast.error("Too many requests. Please wait.")
    }
  },
})
```

## Custom proxy endpoint

By default, HTTP requests are sent to `/api/request`. Override it via `proxyPath.http`:

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

export const caller = createProxyCaller(apiRoutes, {
  proxyPath: { http: "/api/my-proxy" },
})
```

> **Match your server handler**
>
> If you change the proxy URL, make sure your server handler is mounted at the same path. For example, `proxyPath: { http: "/api/my-proxy" }` requires the server handler at `/api/my-proxy`.

## Realtime: SSE and WS proxy paths

`proxyPath` carries per-transport configuration. SSE has a sensible default; WS does not — you must point it at a WebSocket-capable server explicitly.

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

export const caller = createProxyCaller(apiRoutes, {
  proxyPath: {
    http: "/api/request",
    sse: "/api/sse",
    // Required for WS endpoints. On Next.js / Cloudflare / Vercel Edge,
    // run @noverax/courier-server separately and point this at it.
    ws: "wss://realtime.example.com",
  },
})
```

> **WS has no built-in default**
>
> Calling a WS endpoint without `proxyPath.ws` throws `ConfigurationError("WS_PATH_NOT_CONFIGURED")`. See [standalone server deployment](/docs/deployment/standalone-server) for the runtime matrix.

## Per-environment proxy paths

The routes say nothing of where the proxy is: that depends on where the page runs, so the page says it. Read it from what your bundler gives each build — the WS server of local development, a staging host — and leave out what stays the default:

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

export const caller = createProxyCaller(apiRoutes, {
  proxyPath: { ws: process.env.NEXT_PUBLIC_REALTIME_URL },
})
```

A `proxyPath` on `defineApi` is for Studio: its Playground sends a call through the proxy as a page would, at the routes it names there.

```ts title="api/sources.ts (server)"
export const api = defineApi({
  baseURL: "https://api.example.com",
  proxyPath: {
    http: "/api/request",
    sse: "/api/sse",
    ws: "ws://localhost:3001",
  },
  groups: { /* ... */ },
})
```

## Next Steps

- [Exposing Endpoints](/docs/proxy-mode/expose) — mark what a page may call, and the routes Courier writes from the marks
- [Server Handler](/docs/proxy-mode/api-route) — set up the server-side endpoint
- [File Upload](/docs/advanced/upload) — upload files with progress tracking
- [Proxy Mode Overview](/docs/proxy-mode/overview) — architecture and security model
