Skip to content

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.

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 spec
const ApiRoutes = HttpApiBuilder.layer(Api, {
openapiPath: "/openapi.json"
})

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" })

Merge the docs route with your API routes when serving (see Serving & clients):

import { Layer } from "effect"
const AllRoutes = Layer.mergeAll(ApiRoutes, DocsRoute)

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 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" }
}))
{}

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.

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.