# HTTP Client & Methods

How Courier wraps native fetch with typed methods, automatic serialization, and path parameter extraction.

## Overview

Under the hood, Courier uses an `HttpClient` class that wraps the native `fetch` API — no external HTTP libraries. You never create an `HttpClient` directly — it is created internally when a request is executed. What you interact with are the **typed HTTP methods** that HttpClient provides to your endpoint definitions via the `http` object.

## HTTP Methods

An endpoint names its HTTP method with a head — `endpoint.get(path)`, `.post`, `.put`, `.patch`, `.del`, `.head`, `.options` or `.query`. One whose path or body its variables have to be worked into passes `.via()` a closure instead, which receives an `http` object with the same helpers.

```ts title="api/users.ts"
import { courier, defineGroup } from "@noverax/courier/define"

const { endpoint } = courier()

type User = { id: string; name: string; email: string }
type CreateUserVars = { name: string; email: string }
type UpdateUserVars = { id: string; name: string; email: string }

export const users = defineGroup({
  prefix: "users",
  endpoints: {
    // GET /users — variables become search params
    getAll: endpoint.get("").variables<{ page?: number; limit?: number }>().response<User[]>(),

    // GET /users/:id — :id is extracted from variables
    getById: endpoint.get(":id").variables<{ id: string }>().response<User>(),

    // POST /users — variables become JSON body
    create: endpoint.post("").variables<CreateUserVars>().response<User>(),

    // PUT /users/:id — :id extracted, rest becomes JSON body
    update: endpoint.put(":id").variables<UpdateUserVars>().response<User>(),

    // PATCH /users/:id — partial update
    patch: endpoint.patch(":id").variables<{ id: string; email: string }>().response<User>(),

    // DELETE /users/:id
    remove: endpoint.del(":id").variables<{ id: string }>().response<null>(),
  },
})
```

## Method Signatures

The key difference between methods is how they handle variables:

- **GET, DELETE, HEAD** — variables are automatically encoded as search params (`?page=1&limit=10`).
- **POST, PUT, PATCH** — variables are automatically sent as a JSON body.

```typescript
import { courier } from "@noverax/courier/define"

const { endpoint } = courier()

// GET, DELETE, HEAD — variables sent as search params
const search = endpoint.get("search").variables<{ page: number; limit: number }>()
// → GET /users/search?page=1&limit=10

// POST, PUT, PATCH — variables sent as JSON body
const create = endpoint.post("").variables<{ name: string; email: string }>()
// → POST /users  body: { name: "Jane", email: "jane@test.com" }

// A closure, for a path the variables are worked into
const archive = endpoint
  .variables<{ year: number; ids: string[] }>()
  .via((http, vars) => http.post(`archive/${vars.year}`, { ids: vars.ids }))
// → POST /users/archive/2025  body: { ids: [...] }
```

> **Automatic serialization**
>
> You never need to call `JSON.stringify` or build query strings manually. Courier handles encoding based on the HTTP method. You can customize this with `bodySerializer` and `searchParamsSerializer` in `defineApi` options.

## Path Parameters

Use the `:param` syntax in URLs to define path parameters. Courier extracts matching keys from variables, inserts them into the URL, and sends the remaining variables as body or search params:

```ts title="api/comments.ts"
import { courier, defineGroup } from "@noverax/courier/define"

const { endpoint } = courier()

type Comment = { id: string; body: string; author: string }

export const comments = defineGroup({
  prefix: "posts",
  endpoints: {
    // Multiple path params: /posts/:postId/comments/:commentId
    getOne: endpoint.get(":postId/comments/:commentId").variables<{ postId: string; commentId: string }>().response<Comment>(),

    // Path params + body: :postId is extracted, rest becomes body
    create: endpoint.post(":postId/comments").variables<{ postId: string; body: string }>().response<Comment>(),
  },
})

// Usage:
// caller.comments.getOne({ postId: "5", commentId: "12" })
// → GET /posts/5/comments/12

// caller.comments.create({ postId: "5", body: "Great post!" })
// → POST /posts/5/comments  body: { body: "Great post!" }
```

> **Internal regex parser**
>
> Path parameter extraction uses an internal regex-based `:param` parser. Any pattern like `:id`, `:slug`, or `:postId` works out of the box. The parser is cached for performance.

## Response Type

Every HTTP method returns an `HttpResponse<T>` internally. By default, Courier unwraps this and returns just the `data` property. If you need the full response (status code, headers), call through the group's `.raw` accessor:

```typescript
// HttpResponse<T> — what every method returns internally
type HttpResponse<T> = {
  status: number   // HTTP status code (200, 201, 404, ...)
  headers: Headers // Response headers
  data: T          // Parsed response body, typed as T
}

// By default, Courier returns just the data (unwrapped)
const users = await caller.users.getAll()
//    ^? User[]

// Through .raw, you get the full response object
const response = await caller.users.raw.getAll()
//    ^? { status: number; headers: Headers; data: User[] }
```

## Configuring the HTTP Client

The `defineApi` function accepts `BaseOptions` — a comprehensive set of options that configure the underlying HttpClient. These options apply to every endpoint in every group under this source:

```ts title="api/sources.ts"
import { defineApi } from "@noverax/courier/define"
import {
  retryMiddleware,
  authMiddleware,
  logMiddleware,
} from "@noverax/courier/middleware"
import { users } from "./users"
import { posts } from "./posts"

const api = defineApi({
  // Required: your backend URL
  baseURL: process.env.API_URL!,

  // Response format (default: "json")
  responseType: "json",

  // Default data merged into every POST/PUT/PATCH body
  defaultBody: { locale: "en" },

  // Default params merged into every GET request
  defaultQueryParams: { format: "full" },

  // Timeout in milliseconds
  timeout: 30_000,

  // Default headers for all requests
  headers: { "X-Client": "courier" },

  // A newer search cancels the one still in flight
  cancelPrevious: ["users.search"],

  // Middleware chain — retry, auth, logging, etc.
  middleware: [
    logMiddleware(),
    retryMiddleware({ limit: 2 }),
    authMiddleware({ getToken: () => getAuthToken() }),
  ],

  // Groups
  groups: { users, posts },
})
```

> **defaultBody and defaultQueryParams**
>
> `defaultBody` is merged into every POST/PUT/PATCH body, and `defaultQueryParams` is merged into every GET request. Use these for values like locale, API version, or tenant ID that should be sent with every request.

## Next Steps

- [Request Lifecycle](/docs/core-concepts/request) — how requests flow through callers, middleware, and fetch
- [Type Safety](/docs/core-concepts/type-safety) — how types flow from method definitions to your components
- [Error Handling](/docs/core-concepts/error-handling) — handling HTTP errors and the HTTPError class
