OpenAPI
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
Section titled “Generating the spec”OpenApi.fromApi(Api) returns an OpenAPI 3.1 specification object derived from
the definition:
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)When assembling the server, pass openapiPath to HttpApiBuilder.layer to serve
this document at a URL:
import { HttpApiBuilder } from "effect/unstable/httpapi"import { Api } from "../api/Api.ts"
// GET /openapi.json now returns the generated specconst ApiRoutes = HttpApiBuilder.layer(Api, { openapiPath: "/openapi.json"})Serving interactive docs
Section titled “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).
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" })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):
import { Layer } from "effect"
const AllRoutes = Layer.mergeAll(ApiRoutes, DocsRoute)Annotating the spec
Section titled “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.
import { HttpApi, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
// Group-level metadata becomes an OpenAPI tag with a descriptionclass 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` objectclass 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" } })){}Useful annotation fields include title, version, description, summary,
license, externalDocs, servers, and deprecated. For full control, the
transform option lets you post-process the generated spec object, and
override merges arbitrary keys into the node.
Excluding endpoints
Section titled “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:
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 }))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 and Platform.