# defineGroup

Creates a typed endpoint group with a shared URL prefix and endpoint definitions.

## Import

```ts
import { defineGroup } from "@noverax/courier/define"
```

## Signature

```ts
declare function defineGroup<TEndpoints extends OwnEndpoints>(
  config: DefineGroupConfig<TEndpoints>
): DefinedGroup<TEndpoints>

declare function defineGroup<TGroups extends OwnGroups, TEndpoints extends OwnEndpoints>(
  config: DefineNestingGroupConfig<TEndpoints, TGroups>
): DefinedNestingGroup<TEndpoints, TGroups>
```

## Parameters

### DefineGroupConfig

```ts
type DefineGroupConfig<E> = GroupOptions & {
  endpoints: E
}

type DefineNestingGroupConfig<E, G> = GroupOptions & {
  endpoints?: E
  groups: G
}

type GroupOptions = {
  prefix?: string
  pathOptions?: (request?: Request) => Record<string, unknown> | Promise<Record<string, unknown>>
  expose?: boolean
  fetch?: SourceFetch
  webSocket?: SourceWebSocket
  dispatcher?: SourceDispatcher
  proxy?: string // Bun
  tls?: object // Bun
  client?: object // Deno
}
```

- `prefix` — URL path segment prepended to all endpoint URLs in this group.
- `pathOptions` — async function that returns dynamic key-value pairs (headers, cookies) to inject per request.
- `expose` — whether the browser may call the group through the proxy; it overrides the source's, and a group inside another overrides its outer group's.
- `fetch`, `webSocket`, `dispatcher` — how the group's calls, streams and channels reach their server, over the source's: an undici `Agent` holding a client certificate for this group alone, a `ProxyAgent`. A group inside another takes each from the nearest group that sets it, and a call's `.with()` sets its own over both. Bun's `proxy` and `tls` and Deno's `client` are taken the same way. One that cannot send — a `fetch` or a `webSocket` that is not a function, a `dispatcher` with no `dispatch` method — throws `ConfigurationError("SOURCE_TRANSPORT_INVALID")`. See [One group](/docs/advanced/proxies-and-certificates#one-group).
- `groups` — groups inside this one, created with `defineGroup`, to any depth: see [Groups inside groups](#groups-inside-groups). A group made only of groups needs no `endpoints`.
- `endpoints` — record of typed endpoint functions. Any name but `options`, which holds the group's own settings, and `raw` or `with`, which every client answers with its own `.raw` and `.with()`; an endpoint of one of those names is a type error and throws `ConfigurationError("RESERVED_ENDPOINT_NAME")`. The same holds for the endpoints given to `defineApi`, whose groups cannot be named `raw` or `with` either. A path called `options` or `raw` is fine: name the endpoint `getOptions` or `getRaw`. See [Reserved names](/docs/core-concepts/defining-apis#reserved-names).

## Basic Usage

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

const { endpoint } = courier()

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

export const users = defineGroup({
  prefix: "users",
  endpoints: {
    getAll: endpoint.get("").response<User[]>(),
    getById: endpoint.get(":id").variables<{ id: string }>().response<User>(),
    create: endpoint.post("").variables<{ name: string; email: string }>().response<User>(),
    update: endpoint.put(":id").variables<{ id: string; name: string }>().response<User>(),
    remove: endpoint.del(":id").variables<{ id: string }>().response<null>(),
  },
})
```

## Prefix

The prefix is prepended to all endpoint URLs within the group:

```ts
const products = defineGroup({
  prefix: "api/v2/products",
  endpoints: {
    list: endpoint.get("").response<Product[]>(),
    // → GET {baseURL}/api/v2/products

    getById: endpoint.get(":id").variables<{ id: string }>().response<Product>(),
    // → GET {baseURL}/api/v2/products/:id
  },
})
```

## Dynamic Path Options

`pathOptions` returns key-value pairs for URL parameter substitution:

```ts
const tenantUsers = defineGroup({
  prefix: ":tenantId/users",
  pathOptions: async (req) => {
    const tenantId = req?.headers.get("x-tenant-id") || "default"
    return { tenantId }
  },
  endpoints: {
    list: endpoint.get("").response<User[]>(),
    // When tenantId = "acme": GET {baseURL}/acme/users
  },
})
```

> **Path resolution**
>
> Path options from `defineApi()` are resolved first, then group-level `pathOptions` are merged on top. Group options take precedence for overlapping keys. A caller's variables never replace a key `pathOptions` returns.

## Groups inside groups

A group may hold groups, as a source does, and those groups may hold groups of their own. A client reaches one by its names in turn, and its address joins every group's prefix in order:

```ts
const billing = defineGroup({
  prefix: "billing",
  endpoints: {
    summary: endpoint.get("summary").response<{ total: number }>(),
  },
  groups: {
    invoices: defineGroup({
      prefix: "invoices",
      endpoints: {
        list: endpoint.get("").response<Invoice[]>(),
        byId: endpoint.get(":id").variables<{ id: string }>().response<Invoice>(),
      },
    }),
  },
})

const api = defineApi({ baseURL: "https://api.example.com", groups: { billing } })
const client = createCaller(api)

await client.billing.summary() // GET https://api.example.com/billing/summary
await client.billing.invoices.list() // GET https://api.example.com/billing/invoices
```

- A group inside another is named by its **path**, its names joined with dots: `billing.invoices`. The path is what the proxy carries in `_g`, what Studio, mocks and query keys name the group by — `mock("billing.invoices", "list")` — and what a page's [routes](/docs/proxy-mode/expose) name an endpoint by: `"billing.invoices.list"`. A name with a dot in it would read as two, so a group or an endpoint named with one throws `ConfigurationError("DOTTED_NAME")`.
- Each group's `pathOptions` is resolved in turn, from the source's down, a lower group's values replacing a higher one's. `expose` is decided by the nearest group that sets it.
- A group may hold an endpoint and a group of the same name: the client's member is both callable, as the endpoint, and readable, as the group.

## Describing an Endpoint

Each endpoint is built from one of the heads `courier()` returns — `endpoint`, `stream` or `channel` — and addressed with a verb, or with `.via()` when its address is worked out from the variables:

```ts
const { endpoint } = courier()

endpoint.get("").response<User[]>()
// No variables — the caller passes nothing

endpoint.get(":id").variables<{ id: string }>().response<User>()
// Variables required — caller must pass { id: string }
```

## Streams and Channels

A `stream` delivers server-sent events and a `channel` is a WebSocket subscription. Each is either served by the process that defines it, with `.implement()`, or addressed where another process serves it:

| Method | `stream` | `channel` | |
| --- | :---: | :---: | --- |
| `.variables(schema?)` | ✓ | ✓ | What the caller subscribes with |
| `.response(schema?)` | ✓ | ✓ | What each event or message carries |
| `.send(schema?)` | | ✓ | What the caller may send; without it the channel only pushes |
| `.topic(key)` | | ✓ | What subscribers share a topic by, when not all of their variables |
| `.get(path?, options?)` | ✓ | | Where another process serves the stream |
| `.connect(path?, options?)` | | ✓ | Where another process serves the channel |
| `.via(probe, options?)` | ✓ | ✓ | The address worked out from the variables |
| `.implement(handler, options?)` | ✓ | ✓ | Serve it here: a stream's generator of `{ data }` events; a channel's generator of messages, or its `onJoin` / `onMessage` / `onLeave` handlers once it takes `.send()` |

`.use()`, `.headers()`, `.timeout()`, `.retry()` and `.meta()` refine a stream or a channel as they do an endpoint. [Streams & Channels](/docs/advanced/realtime) shows each one served and consumed.

## Endpoint Types

An endpoint is an inert descriptor, not a callable. Three types name one:

```ts
import type {
  ChannelEndpoint,  // ChannelEndpoint<Response, Variables, Send>
  HttpEndpoint,     // HttpEndpoint<Response, Variables, Kind extends "query" | "mutation">
  StreamEndpoint,   // StreamEndpoint<Response, Variables>
} from "@noverax/courier/define"
```

Read the types back off an endpoint with the inference helpers, which is how a
caller knows what a method takes and returns:

```ts
import { courier } from "@noverax/courier/define"
import type { InferResponse, InferVariables } from "@noverax/courier/define"

const { endpoint } = courier()

const getById = endpoint.get(":id").variables<{ id: string }>().response<User>()

type Response = InferResponse<typeof getById>   // User
type Variables = InferVariables<typeof getById> // { id: string }
```

## Related

- [defineApi](/docs/api/define) — composing groups into a source
- [Defining APIs Guide](/docs/core-concepts/defining-apis) — in-depth usage patterns
- [Type Safety](/docs/core-concepts/type-safety) — how types flow from endpoint definitions
