# OpenAPI & Docs UIs

Your `HttpApi` definition already contains everything an OpenAPI document needs:
paths, methods, parameters, request and response schemas, error status codes, and
security schemes. So the spec is **generated**, not hand-maintained — it can never
drift from the running server. You can produce the raw spec, serve it as JSON, or
mount a ready-made documentation UI.

## Generating the spec

`OpenApi.fromApi(Api)` returns an OpenAPI 3.1 specification object derived from
the definition:

```ts
import { OpenApi } from "effect/unstable/httpapi"
import { Api } from "../api/Api.ts"

// A plain OpenAPISpec object — serialize it, write it to disk, publish it, etc.
const spec = OpenApi.fromApi(Api)
// => { openapi: "3.1.0", info: { title, version }, paths, components, security, tags }
```

`fromApi` walks every group and endpoint, emitting OpenAPI tags, paths,
operations, parameters, request bodies, responses, security schemes, and shared
component schemas. The result is **cached per `HttpApi` instance**, so calling it
repeatedly (e.g. once per docs request) is cheap.

When assembling the server, pass `openapiPath` to `HttpApiBuilder.layer` to serve
this document at a URL:

```ts
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { Api } from "../api/Api.ts"

// GET /openapi.json now returns the generated spec
const ApiRoutes = HttpApiBuilder.layer(Api, {
  openapiPath: "/openapi.json"
})
```

## Serving interactive docs

Mount a documentation UI as its own route layer. Both layers serve the OpenAPI
spec and render an explorer at the configured `path` (defaulting to `/docs`).

```ts
import { HttpApiScalar } from "effect/unstable/httpapi"
import { Api } from "../api/Api.ts"

// Scalar assets are inlined by default — no external CDN required.
const DocsRoute = HttpApiScalar.layer(Api, { path: "/docs" })

// Or load Scalar from a CDN instead of inlining it:
// const DocsRoute = HttpApiScalar.layerCdn(Api, { path: "/docs" })
```

  ```ts
import { HttpApiSwagger } from "effect/unstable/httpapi"
import { Api } from "../api/Api.ts"

const DocsRoute = HttpApiSwagger.layer(Api, { path: "/docs" })
```

  Merge the docs route with your API routes when serving (see
[Serving & clients](https://effect.plants.sh/http-api/serving-and-clients/)):

```ts
import { Layer } from "effect"

const AllRoutes = Layer.mergeAll(ApiRoutes, DocsRoute)
```
**Note:** The mounted docs route serves an **HTML page**, not a raw OpenAPI JSON endpoint.
If clients, gateways, or documentation pipelines need the spec directly, expose it
separately with `HttpApiBuilder.layer`'s `openapiPath` option (or call
`OpenApi.fromApi` yourself).

## Annotating the spec

Use `OpenApi.annotations(...)` with `.annotateMerge(...)` to enrich the document.
Annotations apply at every level — the whole API, a group, or an individual
endpoint — and merge together into the final output.

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

// Group-level metadata becomes an OpenAPI tag with a description
class UsersApiGroup extends HttpApiGroup.make("users")
  .add(/* ...endpoints... */)
  .annotateMerge(OpenApi.annotations({
    title: "Users",
    description: "User management endpoints"
  }))
{}

// API-level metadata fills in the document's `info` object
class Api extends HttpApi.make("user-api")
  .add(UsersApiGroup)
  .annotateMerge(OpenApi.annotations({
    title: "Acme User API",
    version: "1.0.0",
    description: "Manage users and check service health",
    license: { name: "MIT" }
  }))
{}
```
**Note:** Security schemes are added automatically from your [middleware](https://effect.plants.sh/http-api/middleware-and-auth/).
A middleware that declares `security: { bearer: HttpApiSecurity.bearer }` produces
the corresponding OpenAPI security scheme, and every guarded endpoint is marked as
requiring it — no extra annotation needed.

## Post-processing the generated spec

`override` and `transform` are also available at the **API level**, letting you
post-process the whole document after generation. Use `override` for static extra
keys and `transform` for programmatic rewrites.

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

// override: shallow-merge top-level keys onto the OpenAPISpec
HttpApi.make("api").annotateMerge(
  OpenApi.annotations({
    override: { "x-logo": { url: "https://acme.com/logo.svg" } }
  })
)
// => spec gains: { "x-logo": { url: "..." } }
```

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

// transform: rewrite the full document — e.g. inject a global security requirement
HttpApi.make("api").annotateMerge(
  OpenApi.annotations({
    transform: (spec) => ({ ...spec, security: [{ bearer: [] }] })
  })
)
// => spec.security === [{ bearer: [] }]
```

For one-off processing without annotations, you can also transform the value
returned by `fromApi` directly:

```ts
import { OpenApi } from "effect/unstable/httpapi"
import { Api } from "../api/Api.ts"

const spec = OpenApi.fromApi(Api)
const published = { ...spec, info: { ...spec.info, summary: "Published build" } }
```

## Excluding endpoints

To keep an endpoint, group, or the whole API out of the generated document — for
internal-only routes — set `exclude: true` in its annotations:

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

HttpApiEndpoint.get("internalMetrics", "/_internal/metrics", {
  success: HttpApiSchema.NoContent
})
  // Present on the server, hidden from the published spec and docs UI
  .annotateMerge(OpenApi.annotations({ exclude: true }))
```

## `OpenApi.annotations` field reference

`OpenApi.annotations(options)` builds a `Context` of annotations from a single
options object; pass it to `.annotateMerge(...)` on an API, group, or endpoint.
Each field maps to a standalone annotation class (also exported for advanced use,
e.g. building a `Context` by hand). Where a field applies depends on the level it
is attached to — noted per field below.

### `title`

Sets the document title (API level), or the tag name (group level). Backed by the
`OpenApi.Title` class.

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

HttpApi.make("api").annotateMerge(OpenApi.annotations({ title: "Acme API" }))
// => info.title === "Acme API"
```

At the group level it overrides the tag name (which otherwise defaults to the
group identifier):

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

HttpApiGroup.make("users").annotateMerge(OpenApi.annotations({ title: "Users" }))
// => tags: [{ name: "Users" }]   (instead of name: "users")
```

### `version`

Sets `info.version` on the document. API level only. Backed by `OpenApi.Version`.

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

HttpApi.make("api").annotateMerge(OpenApi.annotations({ version: "2.3.0" }))
// => info.version === "2.3.0"   (default is "0.0.1")
```

### `description`

Sets a description on the API (`info.description`), a group tag, an endpoint
operation, or a security scheme — depending on where it is attached. Backed by
`OpenApi.Description`.

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

HttpApiEndpoint.get("listUsers", "/users").annotateMerge(
  OpenApi.annotations({ description: "Returns all users, paginated." })
)
// => operation.description === "Returns all users, paginated."
```

### `summary`

Sets a short summary: `info.summary` at the API level, or `operation.summary` on
an endpoint. Backed by `OpenApi.Summary`.

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

HttpApiEndpoint.get("listUsers", "/users").annotateMerge(
  OpenApi.annotations({ summary: "List users" })
)
// => operation.summary === "List users"
```

### `license`

Sets the document license metadata (`info.license`). API level only. Takes an
`OpenAPISpecLicense` (`{ name, url?, ... }`). Backed by `OpenApi.License`.

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

HttpApi.make("api").annotateMerge(
  OpenApi.annotations({ license: { name: "MIT", url: "https://opensource.org/license/mit" } })
)
// => info.license === { name: "MIT", url: "https://opensource.org/license/mit" }
```

### `externalDocs`

Attaches an external documentation link (`{ url, description? }`) to a group tag
or an endpoint operation. Backed by `OpenApi.ExternalDocs`.

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

HttpApiGroup.make("billing").annotateMerge(
  OpenApi.annotations({
    externalDocs: { url: "https://docs.acme.com/billing", description: "Billing guide" }
  })
)
// => tag.externalDocs === { url: "...", description: "Billing guide" }
```

### `servers`

Sets the document server list (`servers`), each entry an `OpenAPISpecServer`
(`{ url, description?, variables? }`). API level only. Backed by `OpenApi.Servers`.

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

HttpApi.make("api").annotateMerge(
  OpenApi.annotations({
    servers: [
      { url: "https://api.acme.com" },
      { url: "https://staging.acme.com", description: "Staging" }
    ]
  })
)
// => servers: [{ url: "https://api.acme.com" }, { url: "https://staging.acme.com", description: "Staging" }]
```

Server URLs may be templated with `variables`, each an `OpenAPISpecServerVariable`
(`{ default, enum?, description? }`):

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

HttpApi.make("api").annotateMerge(
  OpenApi.annotations({
    servers: [{
      url: "https://{region}.api.acme.com",
      variables: { region: { default: "us", enum: ["us", "eu"] } }
    }]
  })
)
```

### `deprecated`

Marks an endpoint operation as deprecated (`operation.deprecated`). Endpoint level.
Backed by `OpenApi.Deprecated`.

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

HttpApiEndpoint.get("legacySearch", "/search").annotateMerge(
  OpenApi.annotations({ deprecated: true })
)
// => operation.deprecated === true
```

### `identifier`

Overrides the generated `operationId` on an endpoint operation. Endpoint level.
Without it, the id is `name` for top-level groups, otherwise `groupId.endpointName`.
Backed by `OpenApi.Identifier`.

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

HttpApiEndpoint.get("listUsers", "/users").annotateMerge(
  OpenApi.annotations({ identifier: "users_list" })
)
// => operation.operationId === "users_list"   (instead of "users.listUsers")
```

### `format`

Sets format metadata used by security schemes — notably the bearer-token format on
an HTTP security scheme (`bearerFormat`). Attach it to a security definition. Backed
by `OpenApi.Format`.

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

OpenApi.annotations({ format: "JWT" })
// => HTTP bearer scheme gains: { bearerFormat: "JWT" }
```

### `override`

Shallowly merges arbitrary extra fields into the generated object (the API spec,
the group tag, or the endpoint operation), via `Object.assign`. Use it to add
fields the generator does not model. Backed by `OpenApi.Override`.

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

HttpApiEndpoint.post("upload", "/upload").annotateMerge(
  OpenApi.annotations({ override: { "x-internal": true } })
)
// => operation gains: { "x-internal": true }
```

### `transform`

A function `(node) => node` applied during generation to the annotated API
document, group tag, or endpoint operation. Use it when a field needs computing
rather than a static merge. Backed by `OpenApi.Transform`.

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

HttpApiEndpoint.get("listUsers", "/users").annotateMerge(
  OpenApi.annotations({
    transform: (op) => ({ ...op, "x-rateLimit": op.deprecated ? 10 : 100 })
  })
)
// => operation gains a computed "x-rateLimit" field
```

`override` is applied **before** `transform`, so a transform sees the already-merged
object.

### `exclude`

Omits the annotated group or endpoint from the generated spec (and therefore the
docs UIs). Backed by the `OpenApi.Exclude` reference (defaults to `false`). See
[Excluding](#excluding-endpoints) below.

### Standalone annotation classes

Every field has a corresponding exported class, useful when building a `Context`
directly (e.g. in shared middleware or schema helpers) instead of via
`OpenApi.annotations`. They are `Context.Service` keys:
`OpenApi.Identifier`, `OpenApi.Title`, `OpenApi.Version`, `OpenApi.Description`,
`OpenApi.License`, `OpenApi.ExternalDocs`, `OpenApi.Servers`, `OpenApi.Format`,
`OpenApi.Summary`, `OpenApi.Deprecated`, `OpenApi.Override`, and `OpenApi.Transform`.
`OpenApi.Exclude` is a `Context.Reference`.

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

// Equivalent to the Title field of OpenApi.annotations({ title: "Users" })
const ctx = Context.make(OpenApi.Title, "Users")
// => Context<OpenApi.Title> carrying the Title annotation
```

## `HttpApiScalar` reference

`HttpApiScalar` mounts a Scalar API reference page on an `HttpRouter`. The route
renders an HTML page that embeds the OpenAPI document and boots Scalar in the
browser. Both layers register a single `GET` route, defaulting to `/docs`.

### `HttpApiScalar.layer`

Mounts Scalar using the **bundled, inlined** Scalar script — no CDN dependency at
runtime (the v4 default). Returns a `Layer` requiring `HttpRouter`.

```ts
import { HttpApiScalar } from "effect/unstable/httpapi"
import { Api } from "../api/Api.ts"

const DocsRoute = HttpApiScalar.layer(Api, {
  path: "/reference",
  scalar: { theme: "purple", layout: "modern" }
})
// => Layer<never, never, HttpRouter> serving Scalar at GET /reference
```

Options: `path` (a `/${string}`, default `/docs`) and `scalar` (a [`ScalarConfig`](#scalarconfig-options)).

### `HttpApiScalar.layerCdn`

Same UI, but loads the Scalar bundle from jsDelivr instead of inlining it. Accepts
an extra `version` option to pin the Scalar package version (defaults to `latest`).

```ts
import { HttpApiScalar } from "effect/unstable/httpapi"
import { Api } from "../api/Api.ts"

const DocsRoute = HttpApiScalar.layerCdn(Api, {
  path: "/docs",
  version: "1.25.0",       // pin for repeatable output
  scalar: { theme: "moon" }
})
// => Layer serving Scalar from https://cdn.jsdelivr.net/npm/@scalar/api-reference@1.25.0/...
```

### `ScalarConfig` options

Passed through as Scalar's HTML configuration. All fields are optional.

- **`theme`** — a [`ScalarThemeId`](#scalarthemeid) color preset.
- **`layout`** — `"modern"` (default look) or `"classic"`.
- **`proxyUrl`** — URL of a request proxy for the in-page API client.
- **`showSidebar`** — whether to show the sidebar.
- **`hideModels`** — hide models from sidebar, search, and content (default `false`).
- **`hideTestRequestButton`** — hide the "Test Request" button (default `false`).
- **`hideSearch`** — hide the sidebar search bar (default `false`).
- **`darkMode`** — whether dark mode is on initially.
- **`forceDarkModeState`** — force `"dark"` or `"light"` regardless of toggle.
- **`hideDarkModeToggle`** — hide the dark-mode toggle.
- **`favicon`** — path to a favicon image, e.g. `"/favicon.svg"`.
- **`customCss`** — custom CSS injected into the page.
- **`baseServerURL`** — origin used for relative server URLs during SSR (browsers
  derive this from `window.location.origin`; server rendering needs it explicit),
  e.g. `"http://localhost:3000"`.
- **`withDefaultFonts`** — load Scalar's Inter / JetBrains Mono fonts (default `true`;
  set `false` when supplying custom fonts).
- **`defaultOpenAllTags`** — open all tags by default (default `false`).
- **`showOperationId`** — display the `operationId` in each operation (default `false`).

```ts
import { HttpApiScalar } from "effect/unstable/httpapi"
import { Api } from "../api/Api.ts"

HttpApiScalar.layer(Api, {
  scalar: {
    theme: "deepSpace",
    layout: "classic",
    hideModels: true,
    showOperationId: true,
    baseServerURL: "http://localhost:3000",
    customCss: ".scalar-app { font-size: 15px }"
  }
})
```

### `ScalarThemeId`

The string union of Scalar theme presets accepted by `ScalarConfig.theme`:
`"alternate"`, `"default"`, `"moon"`, `"purple"`, `"solarized"`, `"bluePlanet"`,
`"saturn"`, `"kepler"`, `"mars"`, `"deepSpace"`, `"laserwave"`, `"none"`.

```ts
import type { HttpApiScalar } from "effect/unstable/httpapi"

const theme: HttpApiScalar.ScalarThemeId = "saturn"
// => valid; "ocean" would be a type error
```

## `HttpApiSwagger` reference

### `HttpApiSwagger.layer`

Mounts Swagger UI on an `HttpRouter`, rendering an HTML page that embeds the
OpenAPI document and boots the bundled Swagger UI bundle. Registers one `GET`
route, defaulting to `/docs`. Returns a `Layer` requiring `HttpRouter`.

```ts
import { HttpApiSwagger } from "effect/unstable/httpapi"
import { Api } from "../api/Api.ts"

const DocsRoute = HttpApiSwagger.layer(Api, { path: "/swagger" })
// => Layer<never, never, HttpRouter> serving Swagger UI at GET /swagger
```

The only option is `path` (a `/${string}`, default `/docs`). Swagger styling and
metadata come from the OpenAPI annotations on the API itself — there is no
per-UI config object like Scalar's.

## The generated spec shape

`OpenApi.fromApi` returns an `OpenApi.OpenAPISpec` — an OpenAPI 3.1.0 document
modeling exactly what the generator produces (not the full OpenAPI spec surface):

```ts
interface OpenAPISpec {
  openapi: "3.1.0"
  info: OpenAPISpecInfo          // { title, version, description?, license?, summary? }
  paths: OpenAPISpecPaths        // Record<routePath, { get?, post?, ... }>
  components: OpenAPIComponents  // { schemas, securitySchemes }
  security: Array<OpenAPISecurityRequirement>
  tags: Array<OpenAPISpecTag>    // one per non-excluded group
  servers?: Array<OpenAPISpecServer>
}
```

Notable details the generator applies:

- **Paths** — `:id` route segments become `{id}` path parameters; the operation's
  `operationId`, `tags`, `parameters`, `responses`, and (for body methods)
  `requestBody` are filled from endpoint metadata.
- **Component schemas** — additional schemas registered on the API must have
  identifiers; those identifiers become component keys, and invalid OpenAPI
  component keys are rejected during generation. A duplicate identifier throws.
- **Security schemes** — derived from security middleware: `Basic` and `Http`
  schemes become `type: "http"`, `ApiKey` becomes `type: "apiKey"` with the
  configured `name`/`in`. A `Description` annotation on the security definition
  becomes the scheme description; a `Format` annotation sets `bearerFormat`.
- **Encodings** — `HttpApiSchema` encodings choose media types (JSON,
  form-url-encoded, text, binary, multipart). No-content schemas emit responses
  without bodies; request/response unions are grouped by status code and content
  type into `anyOf`.

The related model exports — `OpenAPISpecInfo`, `OpenAPISpecTag`,
`OpenAPISpecExternalDocs`, `OpenAPISpecLicense`, `OpenAPISpecServer`,
`OpenAPISpecServerVariable`, `OpenAPISpecPaths`, `OpenAPISpecPathItem`,
`OpenAPISpecOperation`, `OpenAPISpecParameter`, `OpenAPISpecResponses`,
`OpenApiSpecResponse`, `OpenApiSpecContent`, `OpenApiSpecMediaType`,
`OpenAPISpecRequestBody`, `OpenAPIComponents`, `OpenAPISecurityScheme`
(`OpenAPIHTTPSecurityScheme` | `OpenAPIApiKeySecurityScheme`),
`OpenAPISecurityRequirement`, and `OpenAPISpecMethodName` — give you precise types
when post-processing the document.

With the spec, docs UI, and a typed client all derived from one definition, the
contract stays consistent across server, clients, and documentation. For the
broader request/response toolkit, see [HTTP Client](https://effect.plants.sh/http-client/) and
[Platform](https://effect.plants.sh/platform/).