# Options

Full reference of source-level and per-request configuration options.

## Overview

Courier options are split into layers: **BaseOptions** for source-level configuration (applies to all endpoints in a source), **GroupOptions** for a group of endpoints, and **CallOptions** for per-request overrides. Courier uses native `fetch` under the hood — no third-party HTTP clients.

## BaseOptions

Set in `defineApi()`. These apply to every request made through this source:

```ts title="types"
type BaseOptions = {
  // URL & encoding
  baseURL?: string                               // Prefix URL for all requests
  responseType?: "json" | "text" | "blob" | "arrayBuffer" | "formData" // Response parsing (default: "json")
  responseParser?: (response: Response) => Promise<unknown>           // Custom response parser (overrides responseType)
  defaultBody?: Record<string, unknown>          // Merged into every request body
  defaultQueryParams?: Record<string, unknown>   // Merged into every query string
  bodySerializer?: (data: Record<string, unknown>) => BodyInit       // Custom body serializer
  searchParamsSerializer?: SearchParamsSerializer // Custom query serializer ({ stringify, parse })
  allowedHeaders?: string[]                      // Additional header names to forward

  // Behavior
  coalesce?: boolean                             // Identical reads in flight share one request (default: true)
  cancelPrevious?: readonly string[]             // Reads a newer call cancels, such as ["users.search"]
  timeout?: number                               // Request timeout in ms
  headers?: HeadersInit                          // Static headers for all requests
  credentials?: RequestCredentials               // Cookie handling mode
  middleware?: Middleware[]                      // Middleware chain

  // Transport — see Proxies & Certificates
  fetch?: SourceFetch                            // Sends this source's requests and opens its streams (undici's fetch)
  webSocket?: SourceWebSocket                    // Opens its channels' sockets (undici's WebSocket)
  dispatcher?: SourceDispatcher                  // undici Agent or ProxyAgent: a client certificate, an authority, a proxy
  proxy?: string                                 // Bun: the proxy its requests and sockets go through
  tls?: object                                   // Bun: the TLS they use (ca, cert, key, passphrase)
  client?: object                                // Deno: a Deno.HttpClient its requests and streams go through

  // Proxy
  expose?: boolean                               // Whether a page may call its endpoints through the proxy, where nothing nearer says
  proxyPath?: ProxyPath                          // Where the app serves its proxy routes, for Studio's Playground

  // All other fetch options (cache, mode, redirect, signal, etc.)
}
```

An endpoint whose `.via()` closure passes its own `responseType` has its successful answer read that way, the source's `responseParser` included; see [Defining APIs](/docs/core-concepts/defining-apis).

## GroupOptions

Set in `defineGroup()`. These apply to the group's endpoints and to those of the groups inside it:

```ts title="types"
type GroupOptions = {
  prefix?: string                  // Prepended to every endpoint path in the group
  pathOptions?: (request?: Request) => Record<string, string | number | undefined> // Path parameters filled in at request time
  expose?: boolean                 // Whether a page may call its endpoints through the proxy, where nothing nearer says
  groups?: Record<string, DefinedGroup> // The groups inside this one

  // Transport, over the source's — see Proxies & Certificates
  fetch?: SourceFetch
  webSocket?: SourceWebSocket
  dispatcher?: SourceDispatcher    // A client certificate or a proxy for this group alone
  proxy?: string                   // Bun
  tls?: object                     // Bun
  client?: object                  // Deno
}
```

A group inside another takes each transport option from the nearest group that sets it; see [One group](/docs/advanced/proxies-and-certificates#one-group).

## CallOptions

Named with `.with()`, which returns a caller carrying them. These control how a single request behaves:

```ts title="types"
type CallOptions = {
  stream?: boolean               // Return ReadableStream instead of parsed response
  forwardVariables?: boolean     // Pass variables to method body (default: true)
  method?: "GET" | "POST"       // Verb sent to the proxy (default: GET for a read that may be cached, POST for every other call)
  signal?: AbortSignal           // Manual abort signal
  coalesce?: boolean             // false: never join an identical read in flight
  timeout?: number               // Per-request timeout override
  headers?: HeadersInit          // Per-request header overrides
  cache?: RequestCache           // Fetch cache mode
  onUploadProgress?: (progress: {
    percent: number
    transferredBytes: number
    totalBytes: number
  }) => void                     // Upload progress callback
  onDownloadProgress?: (progress: {
    percent: number
    transferredBytes: number
    totalBytes: number
  }) => void                     // Download progress callback

  // Transport for this call alone, over its group's and its source's —
  // refused through the proxy, which sends the call with the server's own
  fetch?: SourceFetch
  webSocket?: SourceWebSocket
  dispatcher?: SourceDispatcher
}
```

The status and headers of an answer are not an option: call through the group's `.raw` accessor, `caller.users.raw.getAll()`, and the call resolves to `{ status, headers, data }`.

`onDownloadProgress` reports against the answer's `content-length`, which the proxy sets on every answer it re-serializes and passes on for a streamed one. When no length is known — a compressed answer — `totalBytes` and `percent` stay 0 until the last event, which reports 100% of what arrived.

## Timeout & retry

Configure request timeouts at the source level and automatic retry behavior via middleware:

```ts title="api/index.ts"
import { defineApi } from "@noverax/courier/define"
import { retryMiddleware } from "@noverax/courier/middleware"

const api = defineApi({
  baseURL: process.env.API_URL ?? "http://localhost:4000",
  groups: { users, posts },

  // Timeout after 10 seconds
  timeout: 10_000,

  // Retry failed requests via middleware
  middleware: [
    retryMiddleware({ limit: 3, delay: 300, retryOn: [408, 502, 503, 504] }),
  ],
})
```

> **Retry via middleware**
>
> Retry logic is handled by `retryMiddleware` from `@noverax/courier/middleware`. It supports exponential backoff with a configurable base delay and custom retry conditions. See [Middleware](/docs/advanced/middleware) for details.

## Caching

The `next` and `cache` fetch options control how the underlying `fetch` call is cached. They work in both direct and proxy modes.

### Direct mode (direct mode)

Options are forwarded directly to the server-side `fetch` call:

```ts title="app/page.tsx"
import { caller } from "@/lib/caller"

// Per-request caching with Next.js ISR
const users = await caller.users.with({ next: { revalidate: 60 } }).getAll()

// Tag-based revalidation
const post = await caller.posts
  .with({ next: { tags: ["posts", `post-${id}`] } })
  .getById({ id })

// Force no caching
const live = await caller.stats.with({ cache: "no-store" }).realtime()
```

### Proxy mode (proxy mode)

In proxy mode a read that declares `.cache()` goes as GET, so its answer can be cached on the way back:

```ts title="components/users.tsx"
const users = await caller.users
  .with({
    next: { revalidate: 60 },
    cache: "force-cache",
  })
  .getAll()
```

The browser's own options — `cache`, `next`, `credentials`, `headers` and the rest of `RequestInit` — apply to the browser's request to `/api/request`. Combined with the `Cache-Control` the proxy answers with, this is what lets the browser, a CDN or the Next.js Data Cache on the page's side reuse the answer.

The proxy itself takes no caching instructions from the browser: a page could otherwise make the server cache one user's answer for everyone. How long an answer may be kept is declared on the endpoint with `.cache({ revalidate })`, and the proxy sets `Cache-Control` from it — `private` when the request carries a credential, shared with `Vary` on the forwarded headers otherwise. Any request header the proxy does not know to be impersonal counts as a credential. What the browser sends on its own counts as impersonal, and so do the headers a reverse proxy, CDN or hosting platform stamps on the way, such as `X-Forwarded-For`, `Forwarded`, `Via`, `CF-Connecting-IP`, `X-Vercel-Id` and `traceparent`. Next.js adds the `X-Forwarded-*` headers to every request itself.

> **Requires GET proxy**
>
> Proxy caching works for a call that goes as GET: a read that declares `.cache()`, unless its variables are sensitive, carry a body or would make the address longer than 2 KB — see [GET or POST](/docs/api/create-proxy-caller#get-or-post). A write asked for over GET is refused 405.

## Headers

Set static headers at the source level or override them per-request. Source-level headers apply to all requests; per-request headers are merged on top:

```ts title="api/index.ts"
import { defineApi } from "@noverax/courier/define"

// Static headers on the source (apply to all requests)
const api = defineApi({
  baseURL: process.env.API_URL ?? "http://localhost:4000",
  groups: { users },
  headers: {
    "X-API-Version": "2",
    "Accept-Language": "en",
  },
})

// Per-request headers
const data = await caller.users
  .with({
    headers: { "X-Custom-Header": "value" },
  })
  .getAll()
```

## Option precedence

When the same option is set at multiple levels, per-request options take the highest priority:

```ts title="api/index.ts"
import { defineApi } from "@noverax/courier/define"

// Options are merged with this precedence (highest wins):
// 1. Per-request options  → caller.reports.with({ timeout: 5000 }).generate(vars)
// 2. Source-level options → defineApi({ timeout: 10000, ... })
// 3. fetch defaults       → native fetch defaults

const api = defineApi({
  baseURL: process.env.API_URL ?? "http://localhost:4000",
  groups: { users, reports },
  timeout: 10_000,  // Default for all requests in this source
})

// Override timeout for a slow endpoint
const report = await caller.reports.with({ timeout: 30_000 }).generate({ type: "annual" })
```

`.with()` can be called at several levels — the client, a group, an endpoint. The later call wins for each option it sets, and headers are merged name by name, whatever their case, so a token set on the client stays when a group adds a header of its own. Headers may be given as a record, a `Headers` or a list of pairs, and they reach a stream opened with `.with()` the same way they reach a request:

```ts
await caller
  .with({ headers: { authorization: `Bearer ${token}` } })
  .reports.with({ headers: { "x-request-id": id } })
  .generate({ type: "annual" }) // sends both headers
```

## Credentials

The `credentials` option controls cookie handling:

- `"include"` — send cookies with cross-origin requests (default in proxy mode)
- `"same-origin"` — send cookies only to same-origin requests
- `"omit"` — never send cookies

## Next steps

- [Middleware](/docs/advanced/middleware) — composable middleware for auth, logging, retry, and more
- [Raw Data Mode](/docs/advanced/raw-data) — using `.raw` to access status codes and headers
- [Streaming](/docs/advanced/streaming) — enabling stream mode for ReadableStream responses
- [Deduplication & Abort](/docs/advanced/abort) — identical reads sharing one request, cancelling a superseded search, aborting a call
- [Proxies & Certificates](/docs/advanced/proxies-and-certificates) — a corporate proxy, an internal certificate authority, a client certificate
