# Defining APIs

An API definition is a pure, dependency-free description of your endpoints. It
contains no handler logic — just schemas — so it can live in a shared package
and be imported by both the server and any clients. This page covers the three
building blocks: **endpoints**, **groups**, and the root **`HttpApi`**.

All three modules live under `effect/unstable/httpapi`:

```ts
import {
  HttpApi,
  HttpApiEndpoint,
  HttpApiError,
  HttpApiGroup,
  HttpApiSchema
} from "effect/unstable/httpapi"
```

## Domain schemas first

Endpoints reference your domain types, so start there. Define them with
[Schema](https://effect.plants.sh/schema/) so they decode and encode automatically.

```ts
// domain/User.ts
import { Schema } from "effect"

// A branded id so a UserId can never be confused with a plain number
export const UserId = Schema.Int.pipe(Schema.brand("UserId"))
export type UserId = typeof UserId.Type

export class User extends Schema.Class<User>("User")({
  id: UserId,
  name: Schema.String,
  email: Schema.String
}) {}
```

Errors are schemas too. Use `Schema.TaggedErrorClass` and attach an HTTP status
code with the `httpApiStatus` annotation so the framework knows how to encode
them on the wire:

```ts
// domain/UserErrors.ts
import { Schema } from "effect"

export class UserNotFound extends Schema.TaggedErrorClass<UserNotFound>()(
  "UserNotFound",
  {},
  // The third argument is annotations: this error responds with 404
  { httpApiStatus: 404 }
) {}

export class SearchQueryTooShort extends Schema.TaggedErrorClass<SearchQueryTooShort>()(
  "SearchQueryTooShort",
  {},
  { httpApiStatus: 422 }
) {
  static readonly minimumLength = 2
}

// A single wrapper error for the Users service. Its `reason` field is a tagged
// union of the domain failures, so the reason-combinators (Effect.catchReason /
// catchReasons / unwrapReason) can target an individual reason by its `_tag`.
export class UsersError extends Schema.TaggedErrorClass<UsersError>()(
  "UsersError",
  {
    reason: Schema.Union([UserNotFound, SearchQueryTooShort])
  }
) {}
```

So `new UsersError({ reason: new UserNotFound() })` is the service's only failure
type, and handlers can pick reasons apart with `Effect.catchReason("UsersError",
"UserNotFound", ...)` (see [Handlers](https://effect.plants.sh/http-api/handlers/)).
**Note:** A success or error schema with no `httpApiStatus` annotation defaults to **200**
for successes and **500** for errors. Set a specific code with the
`httpApiStatus` annotation (shown above) or with `HttpApiSchema.status`.

## Endpoints

An endpoint is created with a method helper (`HttpApiEndpoint.get`, `.post`,
`.put`, `.patch`, `.delete`, `.head`, `.options`). You give it a **name** (used as
the client method name), a **path**, and an options object describing the inputs
and outputs.

```ts
// api/Users.ts
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema } from "effect/unstable/httpapi"
import { User, UserId } from "../domain/User.ts"
import { SearchQueryTooShort, UserNotFound } from "../domain/UserErrors.ts"

export class UsersApiGroup extends HttpApiGroup.make("users")
  .add(
    // Query parameters come from the query string and are all decoded.
    HttpApiEndpoint.get("list", "/", {
      query: { search: Schema.optional(Schema.String) },
      success: Schema.Array(User)
    }),

    // Path parameters must decode *from a string*. Use Schema.decodeTo to
    // bridge from a string schema into your branded domain type.
    HttpApiEndpoint.get("getById", "/:id", {
      params: {
        id: Schema.FiniteFromString.pipe(Schema.decodeTo(UserId))
      },
      success: User,
      // This error returns 404 with no body (see "Empty responses" below).
      error: UserNotFound.pipe(
        HttpApiSchema.asNoContent({ decode: () => new UserNotFound() })
      )
    }),

    // For methods with a body (POST/PUT/PATCH) `payload` is the request body,
    // JSON by default.
    HttpApiEndpoint.post("create", "/", {
      payload: Schema.Struct({
        name: Schema.String,
        email: Schema.String
      }),
      success: User
    }),

    // DELETE has no request body, so `payload` (if any) is read from the query
    // string just like a GET.
    HttpApiEndpoint.delete("remove", "/:id", {
      params: { id: Schema.FiniteFromString.pipe(Schema.decodeTo(UserId)) },
      success: HttpApiSchema.NoContent
    }),

    // An endpoint can declare multiple success or error schemas as an array.
    HttpApiEndpoint.get("search", "/search", {
      // For GET requests `payload` is read from the query string.
      payload: { search: Schema.String },
      success: [
        Schema.Array(User),
        // Negotiate a CSV response with a different content type
        Schema.String.pipe(HttpApiSchema.asText({ contentType: "text/csv" }))
      ],
      error: [
        SearchQueryTooShort.pipe(
          HttpApiSchema.asNoContent({ decode: () => new SearchQueryTooShort() })
        ),
        // Built-in errors cover common HTTP cases
        HttpApiError.RequestTimeoutNoContent
      ]
    })
  )
  .prefix("/users")
{}
```

### Inputs: `params`, `query`, `headers`, `payload`

Each is optional and accepts either a `Schema.Struct` (or any single `Schema`)
or a plain record of field schemas (a shorthand for a struct):

| Option    | Source                          | Notes |
| --------- | ------------------------------- | ----- |
| `params`  | Path segments (`/:id`)          | Must decode from a string. |
| `query`   | Query string                    | Decoded individually. |
| `headers` | Request headers                 | Decoded individually. |
| `payload` | Request **body** for `WithBody` methods, **query string** for no-body methods | JSON by default for body methods; form-url-encoded for no-body methods. |
**Note:** Because path parameters, query values, and headers arrive as strings, their
schemas must be **string-decodable**. Each `params`/`headers` field must encode
to `string | undefined`; each `query` field to `string | ReadonlyArray<string> |
undefined`. Use schemas like `Schema.FiniteFromString`,
`Schema.NumberFromString`, or `Schema.decodeTo` to convert into richer types.
For no-body methods (`GET`, `HEAD`, `DELETE`, `OPTIONS`), `payload` fields follow
the same query-string string-decodability rule.

By default codecs are applied automatically: `params`, `query`, and `headers`
use string-tree codecs, body payloads use JSON codecs, and no-body payloads use
form-url-encoded codecs. Pass `{ disableCodecs: true }` in the options object if
you have already wrapped the schemas in the right transport codec yourself.

### Outputs: `success` and `error`

`success` defaults to `HttpApiSchema.NoContent` (a `204` with no body). Provide a
schema — or an array of schemas — to describe the response body. `error` declares
the failure schemas; their status codes come from the `httpApiStatus` annotation
or from `HttpApiSchema.status`. When you pass an array, each schema can declare a
different status code, and the server/clients select by status.

### Empty responses and status codes

`HttpApiSchema` controls encoding and status:

```ts
import { HttpApiSchema } from "effect/unstable/httpapi"
import { Schema } from "effect"

HttpApiSchema.NoContent           // 204, no body
HttpApiSchema.Created             // 201, no body
HttpApiSchema.Accepted            // 202, no body

// Force a status code onto any schema
const Gone = Schema.Void.pipe(HttpApiSchema.status(410))

// Turn an error schema into a no-content response, supplying a decoder used
// when reconstructing the error on the client.
UserNotFound.pipe(HttpApiSchema.asNoContent({ decode: () => new UserNotFound() }))
```

Other encoders include `asText`, `asJson`, `asFormUrlEncoded`, `asUint8Array`,
`asMultipart`, and `asMultipartStream` for content-type negotiation and binary
or file payloads — all documented in the
[HttpApiSchema reference](#httpapischema-reference) below.

## Grouping endpoints

`HttpApiGroup.make` bundles endpoints. Groups support `.prefix(...)` to add a
common path segment, `.middleware(...)` to apply [middleware](https://effect.plants.sh/http-api/middleware-and-auth/)
to every endpoint, and `.annotateMerge(...)` for OpenAPI metadata.

```ts
import { HttpApiGroup, OpenApi } from "effect/unstable/httpapi"

class UsersApiGroup extends HttpApiGroup.make("users")
  .add(/* ...endpoints... */)
  .prefix("/users")
  .annotateMerge(OpenApi.annotations({
    title: "Users",
    description: "User management endpoints"
  }))
{}
```
**Caution:** `prefix`, `middleware`, `annotateEndpoints`, and `annotateEndpointsMerge` only
affect endpoints that are **already present** when they are called. Endpoints
added afterward are untouched. Adding an endpoint with the same name replaces
the existing one.

### Top-level groups

Pass `{ topLevel: true }` to attach a group's endpoints directly to the root of
the generated client, instead of namespacing them under the group name. This is
handy for system endpoints like health checks:

```ts
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/unstable/httpapi"

// Reachable as `client.health()` rather than `client.system.health()`
export class SystemApi extends HttpApiGroup.make("system", { topLevel: true }).add(
  HttpApiEndpoint.get("health", "/health", {
    success: HttpApiSchema.NoContent
  })
) {}
```

## The root API

`HttpApi.make` creates the root. Add groups with `.add(...)`, and annotate the
whole API with OpenAPI metadata:

```ts
// api/Api.ts
import { HttpApi, OpenApi } from "effect/unstable/httpapi"
import { SystemApi } from "./System.ts"
import { UsersApiGroup } from "./Users.ts"

// This is the value you serve and generate clients from.
export class Api extends HttpApi.make("user-api")
  .add(UsersApiGroup)
  .add(SystemApi)
  .annotateMerge(OpenApi.annotations({ title: "Acme User API" }))
{}
```

With the definition in place, the next step is to [implement
handlers](https://effect.plants.sh/http-api/handlers/) for each endpoint.

---

The remainder of this page is an exhaustive reference for every public API used
to define endpoints, schemas, errors, groups, and the root API.

## HttpApiEndpoint reference

An `HttpApiEndpoint` is a **declaration**, not a handler: it records a name, HTTP
method, path, request schemas (`params`/`query`/`headers`/`payload`), response
schemas (`success`/`error`), endpoint middleware, and annotations. Builders use
it to decode requests, type handler input, encode responses, and generate
OpenAPI/clients.

### Method constructors: `get` / `post` / `put` / `patch` / `delete` / `head` / `options`

Each helper builds an endpoint for a specific HTTP method. The signature is
`(name, path, options?)`. `delete` is exported under the reserved name, so you
call it as a property: `HttpApiEndpoint.delete(...)`.

```ts
import { Schema } from "effect"
import { HttpApiEndpoint } from "effect/unstable/httpapi"

HttpApiEndpoint.get("list", "/users")        // method "GET"
HttpApiEndpoint.post("create", "/users")     // method "POST"
HttpApiEndpoint.put("replace", "/users/:id") // method "PUT"
HttpApiEndpoint.patch("update", "/users/:id")// method "PATCH"
HttpApiEndpoint.delete("remove", "/users/:id")// method "DELETE"
HttpApiEndpoint.head("exists", "/users/:id") // method "HEAD"
HttpApiEndpoint.options("opts", "/users")    // method "OPTIONS"

const ep = HttpApiEndpoint.post("create", "/users", {
  payload: Schema.Struct({ name: Schema.String }),
  success: Schema.Struct({ id: Schema.Number })
})
ep.name   // => "create"
ep.method // => "POST"
ep.path   // => "/users"
```
**Note:** There is no separate `del`/`delete` factory other than this method helper — the
module simply re-exports the `DELETE` constructor under the name `delete`.

### The endpoint options object

The optional third argument fully describes the route. All fields are optional:

```ts
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiSchema, HttpApiError } from "effect/unstable/httpapi"

HttpApiEndpoint.post("create", "/users/:teamId", {
  // Path parameters from `/:teamId`. A record is shorthand for Schema.Struct.
  params: { teamId: Schema.String },
  // Query string values.
  query: { dryRun: Schema.optional(Schema.String) },
  // Request headers.
  headers: { "x-trace-id": Schema.optional(Schema.String) },
  // Request body (JSON by default for body methods).
  payload: Schema.Struct({ name: Schema.String }),
  // Success response schema(s). Single schema or array.
  success: Schema.Struct({ id: Schema.Number }),
  // Error response schema(s). Single schema or array.
  error: HttpApiError.Conflict,
  // Set true if your schemas are already wrapped in transport codecs.
  disableCodecs: false
})
```

`success` defaults to `HttpApiSchema.NoContent`. `params`, `query`, `headers`,
`payload`, and `error` default to "none". Multiple `payload` schemas may share a
content type only if they use the same encoding strategy; multipart payloads
cannot be combined under the same content type.

### `make`

Creates the method-specific constructor used by `get`/`post`/etc. Use it
directly only when you need a method the helpers do not cover.

```ts
import { HttpApiEndpoint } from "effect/unstable/httpapi"

const trace = HttpApiEndpoint.make("TRACE")
const ep = trace("trace", "/debug")
ep.method // => "TRACE"
```

### `isHttpApiEndpoint`

Type guard that narrows an unknown value to an endpoint.

```ts
import { HttpApiEndpoint } from "effect/unstable/httpapi"

const ep = HttpApiEndpoint.get("list", "/")
HttpApiEndpoint.isHttpApiEndpoint(ep) // => true
HttpApiEndpoint.isHttpApiEndpoint({}) // => false
```

### `.prefix`

Returns a new endpoint with a path prefix prepended.

```ts
import { HttpApiEndpoint } from "effect/unstable/httpapi"

const ep = HttpApiEndpoint.get("list", "/").prefix("/users")
ep.path // => "/users/"
```

### `.middleware`

Attaches an [`HttpApiMiddleware`](https://effect.plants.sh/http-api/middleware-and-auth/) tag to a single
endpoint, merging its required services and declared errors into the endpoint.

```ts
// declare a middleware tag elsewhere, then:
// HttpApiEndpoint.get("list", "/").middleware(Authorization)
```

### `.annotate` / `.annotateMerge`

Attach annotations (e.g. OpenAPI metadata) to a single endpoint. `annotate` adds
one `Context.Key`/value pair; `annotateMerge` merges an entire `Context`.

```ts
import { Context } from "effect"
import { HttpApiEndpoint } from "effect/unstable/httpapi"
import { OpenApi } from "effect/unstable/httpapi"

const ep = HttpApiEndpoint.get("list", "/")
  .annotateMerge(OpenApi.annotations({ description: "List all users" }))
// ep.annotations is a Context.Context<never>
Context.isContext(ep.annotations) // => true
```

## HttpApiSchema reference

`HttpApiSchema` does not define routes or perform IO — it annotates `Schema`
values so the surrounding HTTP API tooling can pick status codes, content types,
and body codecs. Keep `Schema` as the source of validation/transformation, then
layer on these helpers.

### `NoContent`

A `Schema.Void` annotated with status **204**. The default `success` schema.

```ts
import { HttpApiSchema } from "effect/unstable/httpapi"

HttpApiSchema.NoContent // => Schema.Void with httpApiStatus 204
```

### `Created`

A `Schema.Void` annotated with status **201**.

```ts
import { HttpApiSchema } from "effect/unstable/httpapi"

HttpApiSchema.Created // => Schema.Void with httpApiStatus 201
```

### `Accepted`

A `Schema.Void` annotated with status **202**.

```ts
import { HttpApiSchema } from "effect/unstable/httpapi"

HttpApiSchema.Accepted // => Schema.Void with httpApiStatus 202
```

### `Empty`

Creates a `Schema.Void` with an arbitrary status code — the building block behind
`NoContent`/`Created`/`Accepted`.

```ts
import { HttpApiSchema } from "effect/unstable/httpapi"

const Gone = HttpApiSchema.Empty(410) // => Schema.Void with httpApiStatus 410
```

### `status`

Sets the `httpApiStatus` annotation on any schema. Accepts a numeric code or a
common status literal (e.g. `"Created"`, `"NotFound"`).

```ts
import { Schema } from "effect"
import { HttpApiSchema } from "effect/unstable/httpapi"

const A = Schema.String.pipe(HttpApiSchema.status(418))      // => 418
const B = Schema.String.pipe(HttpApiSchema.status("Created"))// => 201
```

### `asText`

Marks a schema (whose `Encoded` is a `string`) as a `text/plain` payload or
response. Pass `contentType` to override.

```ts
import { Schema } from "effect"
import { HttpApiSchema } from "effect/unstable/httpapi"

// encode: value -> "text/plain" body ; decode: body string -> value
Schema.String.pipe(HttpApiSchema.asText())
// negotiate a CSV content type while still sending text
Schema.String.pipe(HttpApiSchema.asText({ contentType: "text/csv" }))
```

### `asJson`

Marks a schema as a JSON payload/response (`application/json`). This is already
the default for body payloads and responses; use it to be explicit or to set a
custom JSON-family content type.

```ts
import { Schema } from "effect"
import { HttpApiSchema } from "effect/unstable/httpapi"

Schema.Struct({ ok: Schema.Boolean }).pipe(HttpApiSchema.asJson())
Schema.Struct({ ok: Schema.Boolean }).pipe(
  HttpApiSchema.asJson({ contentType: "application/vnd.api+json" })
)
```

### `asFormUrlEncoded`

Marks a schema as `application/x-www-form-urlencoded`. The schema's encoded side
must be a record of strings. This is the default payload encoding for no-body
methods.

```ts
import { Schema } from "effect"
import { HttpApiSchema } from "effect/unstable/httpapi"

// decode: "a=1&b=2" -> { a: "1", b: "2" } ; encode: record -> form body
Schema.Struct({ a: Schema.String, b: Schema.String }).pipe(
  HttpApiSchema.asFormUrlEncoded()
)
```

### `asUint8Array`

Marks a schema (whose `Encoded` is a `Uint8Array`) as a binary
`application/octet-stream` payload/response.

```ts
import { Schema } from "effect"
import { HttpApiSchema } from "effect/unstable/httpapi"

Schema.Uint8Array.pipe(HttpApiSchema.asUint8Array())
Schema.Uint8Array.pipe(HttpApiSchema.asUint8Array({ contentType: "image/png" }))
```

### `asMultipart`

Marks a payload schema as buffered `multipart/form-data`. The whole body is read
into memory before the handler runs. Accepts optional `Multipart` limits.

```ts
import { Schema } from "effect"
import { HttpApiSchema } from "effect/unstable/httpapi"

const Upload = Schema.Struct({
  title: Schema.String
}).pipe(HttpApiSchema.asMultipart())
// handler receives the fully-decoded value
```

### `asMultipartStream`

Marks a payload schema as a streaming multipart payload. The handler receives a
`Stream.Stream<Multipart.Part, Multipart.MultipartError>` instead of a buffered
value — useful for large uploads.

```ts
import { Schema } from "effect"
import { HttpApiSchema } from "effect/unstable/httpapi"

const Upload = Schema.Struct({ field: Schema.String }).pipe(
  HttpApiSchema.asMultipartStream()
)
// request.payload is a Stream of Multipart.Part
```

### `asNoContent`

Marks a schema as a no-content response while still producing a useful decoded
value on the client. The server encodes `void`; the client calls `decode` to
reconstruct a value when the response has no body.

```ts
import { Schema } from "effect"
import { HttpApiSchema } from "effect/unstable/httpapi"

// wire: empty body ; client decodes to { acknowledged: true }
const Ack = Schema.Struct({ acknowledged: Schema.Boolean }).pipe(
  HttpApiSchema.asNoContent({ decode: () => ({ acknowledged: true }) })
)
```

### `isNoContent`

Predicate over a schema AST; returns `true` when the schema represents a
no-content (`void`) response, including transformations whose encoded target is
`void`.

```ts
import { HttpApiSchema } from "effect/unstable/httpapi"

HttpApiSchema.isNoContent(HttpApiSchema.NoContent.ast) // => true
```

### `getStatusSuccess` / `getStatusError`

Resolve the effective status code for a schema AST. `getStatusSuccess` defaults
to `200`, `getStatusError` to `500`, when no `httpApiStatus` annotation is
present.

```ts
import { Schema } from "effect"
import { HttpApiSchema } from "effect/unstable/httpapi"

HttpApiSchema.getStatusSuccess(Schema.String.ast)                 // => 200
HttpApiSchema.getStatusError(Schema.String.ast)                   // => 500
HttpApiSchema.getStatusSuccess(HttpApiSchema.Created.ast)         // => 201
```

### `getPayloadEncoding` / `getResponseEncoding`

Resolve the encoding (`Json` / `FormUrlEncoded` / `Text` / `Uint8Array` /
`Multipart`) chosen for a payload or response. Payload encoding depends on the
HTTP method: body methods default to JSON, no-body methods to form-url-encoded.
Response encoding rejects multipart.

```ts
import { Schema } from "effect"
import { HttpApiSchema } from "effect/unstable/httpapi"

HttpApiSchema.getPayloadEncoding(Schema.Struct({}).ast, "POST")._tag // => "Json"
HttpApiSchema.getPayloadEncoding(Schema.Struct({}).ast, "GET")._tag  // => "FormUrlEncoded"
HttpApiSchema.getResponseEncoding(Schema.String.ast)._tag            // => "Json"
```

## HttpApiError reference

`HttpApiError` provides reusable `Schema.ErrorClass` values for common HTTP
status codes. Each carries an `httpApiStatus` annotation and renders as an empty
response with the matching status when used directly as a server response.
Declaring one as an endpoint `error` tells the server how to encode it and tells
generated clients how to decode it.

```ts
import { HttpApiEndpoint, HttpApiError } from "effect/unstable/httpapi"

HttpApiEndpoint.get("getById", "/:id", {
  error: HttpApiError.NotFound // responds 404
})
```

### Status error classes

Each class is a tagged error with a fixed status code:

| Class                 | Status | Class                 | Status |
| --------------------- | ------ | --------------------- | ------ |
| `BadRequest`          | 400    | `Gone`                | 410    |
| `Unauthorized`        | 401    | `InternalServerError` | 500    |
| `Forbidden`           | 403    | `NotImplemented`      | 501    |
| `NotFound`            | 404    | `ServiceUnavailable`  | 503    |
| `MethodNotAllowed`    | 405    |                       |        |
| `NotAcceptable`       | 406    |                       |        |
| `RequestTimeout`      | 408    |                       |        |
| `Conflict`            | 409    |                       |        |

```ts
import { HttpApiError } from "effect/unstable/httpapi"

const err = new HttpApiError.Conflict()
err._tag // => "Conflict"
// HttpApiError.Conflict has httpApiStatus 409

// `BadRequest` ships a reusable singleton instance
HttpApiError.BadRequest.singleton._tag // => "BadRequest"
```

### `*NoContent` variants

Every status error has a matching `*NoContent` export: a schema that decodes an
empty response of that status into the corresponding error value. Use these when
the wire response intentionally has no body but clients should still decode a
typed error.

```ts
import { HttpApiEndpoint, HttpApiError } from "effect/unstable/httpapi"

HttpApiEndpoint.get("getById", "/:id", {
  // empty 404 on the wire, decoded to a NotFound error on the client
  error: HttpApiError.NotFoundNoContent
})
```

Available variants: `BadRequestNoContent`, `UnauthorizedNoContent`,
`ForbiddenNoContent`, `NotFoundNoContent`, `MethodNotAllowedNoContent`,
`NotAcceptableNoContent`, `RequestTimeoutNoContent`, `ConflictNoContent`,
`GoneNoContent`, `InternalServerErrorNoContent`, `NotImplementedNoContent`, and
`ServiceUnavailableNoContent`.

### `HttpApiSchemaError`

The error the HTTP API runtime raises when a request component fails schema
decoding. It records which component failed via `kind` (`"Params"`, `"Headers"`,
`"Query"`, `"Body"`, or `"Payload"`) and responds as an empty `400 Bad Request`.

```ts
import { HttpApiError } from "effect/unstable/httpapi"

declare const value: unknown
if (HttpApiError.HttpApiSchemaError.is(value)) {
  value.kind // => one of "Params" | "Headers" | "Query" | "Body" | "Payload"
}
```

## HttpApiGroup reference

An `HttpApiGroup` is a named collection of endpoints — the boundary for a single
resource or feature area. In generated clients a regular group becomes a named
namespace; a `topLevel` group exposes its endpoint methods directly.

### `make`

Creates an empty group with the given identifier. Pass `{ topLevel: true }` to
flatten its endpoints onto the client root.

```ts
import { HttpApiGroup } from "effect/unstable/httpapi"

HttpApiGroup.make("users")                       // namespaced as `client.users.*`
HttpApiGroup.make("system", { topLevel: true })  // methods on the client root

const g = HttpApiGroup.make("users")
g.identifier // => "users"
g.topLevel   // => false
```

### `.add`

Adds one or more endpoints. Adding an endpoint whose `name` already exists
replaces it.

```ts
import { HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi"

const g = HttpApiGroup.make("users").add(
  HttpApiEndpoint.get("list", "/"),
  HttpApiEndpoint.get("getById", "/:id")
)
Object.keys(g.endpoints) // => ["list", "getById"]
```

### `.prefix`

Prepends a path prefix to **every endpoint already in the group**.

```ts
import { HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi"

const g = HttpApiGroup.make("users")
  .add(HttpApiEndpoint.get("list", "/"))
  .prefix("/users")
g.endpoints.list.path // => "/users/"
```

### `.middleware`

Applies an [`HttpApiMiddleware`](https://effect.plants.sh/http-api/middleware-and-auth/) to every endpoint
currently in the group. Endpoints added later are not affected.

```ts
// HttpApiGroup.make("users").add(...).middleware(Authorization)
```

### `.annotate` / `.annotateMerge`

Attach annotations to the **group itself** (not its endpoints). `annotate` adds a
single key/value; `annotateMerge` merges a `Context`. Use
`.annotateEndpoints` / `.annotateEndpointsMerge` to annotate every current
endpoint instead.

```ts
import { HttpApiGroup, OpenApi } from "effect/unstable/httpapi"

HttpApiGroup.make("users")
  .annotateMerge(OpenApi.annotations({ title: "Users" }))
```

### `isHttpApiGroup`

Type guard narrowing an unknown value to a group.

```ts
import { HttpApiGroup } from "effect/unstable/httpapi"

HttpApiGroup.isHttpApiGroup(HttpApiGroup.make("users")) // => true
HttpApiGroup.isHttpApiGroup({})                         // => false
```

## HttpApi reference

The root `HttpApi` is the shared contract consumed by server builders, generated
clients, URL builders, OpenAPI generation, and reflection. Handlers are supplied
later with `HttpApiBuilder.group`, and the API is registered with
`HttpApiBuilder.layer` (see [Serving & clients](https://effect.plants.sh/http-api/serving-and-clients/)).

### `make`

Creates an empty root API with a stable identifier.

```ts
import { HttpApi } from "effect/unstable/httpapi"

const api = HttpApi.make("user-api")
api.identifier // => "user-api"
```

### `.add`

Adds one or more groups. Group identifiers are keys — adding a group with the
same identifier replaces the previous one.

```ts
import { HttpApi, HttpApiGroup } from "effect/unstable/httpapi"

const api = HttpApi.make("user-api")
  .add(HttpApiGroup.make("users"))
  .add(HttpApiGroup.make("system", { topLevel: true }))
Object.keys(api.groups) // => ["users", "system"]
```

### `.addHttpApi`

Merges another `HttpApi`'s groups into this one, merging the added API's
annotations into its groups.

```ts
import { HttpApi } from "effect/unstable/httpapi"

const a = HttpApi.make("a")
const b = HttpApi.make("b")
const merged = a.addHttpApi(b)
```

### `.prefix` / `.middleware`

`prefix` prepends a path to every endpoint in every group currently present;
`middleware` applies an [`HttpApiMiddleware`](https://effect.plants.sh/http-api/middleware-and-auth/) to
them. Both only affect groups/endpoints present when called.

```ts
import { HttpApi, HttpApiGroup, HttpApiEndpoint } from "effect/unstable/httpapi"

HttpApi.make("user-api")
  .add(HttpApiGroup.make("users").add(HttpApiEndpoint.get("list", "/")))
  .prefix("/api")
```

### `.annotate` / `.annotateMerge`

Attach annotations to the whole API (e.g. OpenAPI title/description).

```ts
import { HttpApi, OpenApi } from "effect/unstable/httpapi"

HttpApi.make("user-api")
  .annotateMerge(OpenApi.annotations({ title: "Acme User API" }))
```

### `isHttpApi`

Type guard narrowing an unknown value to an `HttpApi`.

```ts
import { HttpApi } from "effect/unstable/httpapi"

HttpApi.isHttpApi(HttpApi.make("user-api")) // => true
HttpApi.isHttpApi({})                       // => false
```

### `reflect`

Walks the final group and endpoint metadata — with merged annotations,
status-indexed success/error schemas, and middleware — invoking `onGroup` and
`onEndpoint` callbacks. This is what OpenAPI generation and client derivation
build on; reach for it when writing your own tooling.

```ts
import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi"

const api = HttpApi.make("user-api").add(
  HttpApiGroup.make("users").add(HttpApiEndpoint.get("list", "/"))
)

HttpApi.reflect(api, {
  onGroup: ({ group }) => {
    group.identifier // => "users"
  },
  onEndpoint: ({ endpoint, successes, errors }) => {
    endpoint.name // => "list"
    // `successes` / `errors` are ReadonlyMap<status, schemas>
    successes.has(204) // => true (NoContent default)
  }
})
```

### `AdditionalSchemas`

A `Context.Service` holding extra schemas to emit under `components/schemas` in
generated OpenAPI. Each schema must have an `identifier` annotation. Provide it
when registering the API (see [OpenAPI](https://effect.plants.sh/http-api/openapi/)).

```ts
import { Layer, Schema } from "effect"
import { HttpApi } from "effect/unstable/httpapi"

const layer = Layer.succeed(HttpApi.AdditionalSchemas, [
  Schema.String.annotate({ identifier: "MyString" })
])
```