# Request Lifecycle

How requests flow through callers, middleware, and the HTTP client in direct and proxy modes.

## Overview

Every API call in Courier follows a well-defined lifecycle. There are two transport modes — **Direct** and **Proxy** — but both converge on the same internal `executeRequest` function. Understanding this flow helps you debug issues, write middleware, and choose the right mode for your use case.

## Two Modes

| Factory | Mode | Runtime | Use Case |
|---------|------|---------|----------|
| `createCaller(api)` | Direct | Any (Node.js, Bun, Deno, browser) | Server Components, Server Actions, scripts, CLI |
| `createProxyCaller(apiRoutes)` | Proxy | Browser | Client Components — hides backend URL, handles CORS, carries the cookies you name |

## Request Flow

```
Direct:  createCaller(api) → caller.users.getAll() → executeRequest → middleware → HttpClient → fetch → backend

Proxy:   createProxyCaller(apiRoutes) → caller.users.getAll() → GET or POST /api/request → handler → executeRequest → middleware → HttpClient → fetch → backend
```

In direct mode, the caller invokes `executeRequest` directly with the `api` object. In proxy mode, the browser sends the request payload to your server's proxy route (e.g., `/api/request`), which then calls `executeRequest` on the server side.

## Setting Up Callers

### Direct mode — server-side (direct mode)

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

export const caller = createCaller(api)
```

```ts
// Works anywhere with access to the api object
const users = await caller.users.getAll()
//    ^? User[]

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

> **When to use direct mode**
>
> Use direct mode whenever you have access to the `api` object: Server Components, Server Actions, API routes, CLI scripts, cron jobs, or any non-browser runtime.

### Proxy mode — browser-side (proxy mode)

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

export const caller = createProxyCaller(apiRoutes)
```

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

The caller takes the routes Courier writes from the endpoints you marked `.expose()` — see [Exposing Endpoints](/docs/proxy-mode/expose).

> **Type-only import**
>
> Proxy mode imports `type { api }` — only the type information crosses to the client. The actual `api` object (with your backend URL and secrets) stays on the server.

## The Dot-Notation API

Both `createCaller` and `createProxyCaller` return a typed client with dot-notation access to your API groups and endpoints:

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

TypeScript validates group and method names at compile time:

```ts
caller.users.getAll()              // group: "users", method: "getAll"
caller.users.getById({ id: "1" }) // group: "users", method: "getById"
caller.posts.create(vars)         // group: "posts", method: "create"
```

## Call Options

Each endpoint supports runtime options that control how the request is executed:

```ts
// Raw response — returns { status, headers, data } instead of just data
const response = await caller.users.raw.getAll()
//    ^? { status: number; headers: Headers; data: User[] }

// Stream mode — the body arrives as a ReadableStream instead of parsed
const body = 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()

// Upload with 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()

// Custom headers and timeout per call
await caller.users
  .with({
    headers: { "X-Request-Id": crypto.randomUUID() },
    timeout: 5000,
  })
  .getAll()
```

### CallOptions Reference

| Option | Type | Description |
|--------|------|-------------|
| `stream` | `boolean` | Return the unread body, a `ReadableStream`, for streaming |
| `method` | `"GET" \| "POST"` | Proxy HTTP method (GET for cacheable reads) |
| `signal` | `AbortSignal` | Cancel the request |
| `timeout` | `number` | Request timeout in ms |
| `headers` | `HeadersInit` | Additional headers for this call |
| `onUploadProgress` | `(progress) => void` | Upload progress callback (XHR in the browser) |
| `onDownloadProgress` | `(progress) => void` | Download progress callback |
| `forwardVariables` | `boolean` | Pass variables to the HTTP method (default: `true`) |

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

## Internal Lifecycle

When `executeRequest` receives a request payload, it goes through these steps:

1. **Resolve source** — looks up the source object in the `sources` map
2. **Resolve endpoint** — finds the group and endpoint function by parsing the `"group:method"` key
3. **Validate variables** — if the endpoint has a schema attached (via `.variables(schema)`), validates the input
4. **Run middleware chain** — executes middleware in array order, each wrapping the next
5. **Execute endpoint** — creates `HttpClient`, calls the endpoint function with `http` methods and variables
6. **Validate response** — if the endpoint has a response schema, validates the output
7. **Build response** — formats the result based on context (proxy Response vs direct data)

```
executeRequest
  ├─ resolve source & endpoint
  ├─ validate variables (if schema)
  ├─ middleware[0]
  │   └─ middleware[1]
  │       └─ middleware[n]
  │           └─ via(http, vars) → HttpClient.fetch → backend
  ├─ validate response (if schema)
  └─ return data (or Response for proxy)
```

## Context (Direct Mode)

In direct mode, a caller built with a `Context` forwards the incoming request's headers (useful in Server Components and API routes). A context belongs to one incoming request, so the caller is built for that request:

```ts
import { createCaller } from "@noverax/courier"
import { api } from "@/api/sources"

// In a Server Component or API route
export async function GET(request: Request): Promise<Response> {
  const caller = createCaller(api, { context: { req: request } })
  const user = await caller.users.getById({ id: "1" })
  return Response.json(user)
}
```

The context object accepts:

| Property | Type | Description |
|----------|------|-------------|
| `req` | `Request` | Incoming request (for header forwarding) |
| `res` | `Response` | When set, `executeRequest` returns a `Response` object |
| `headers` | `Headers` | Additional headers to merge |

## Next Steps

- [Caller & Proxy](/docs/core-concepts/caller) — detailed options for both caller factories
- [HTTP Client & Methods](/docs/core-concepts/instance) — how endpoint functions use HTTP methods
- [Middleware](/docs/middleware/overview) — add retry, auth, logging to the request lifecycle
