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:
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema} from "effect/unstable/httpapi"Domain schemas first
Section titled “Domain schemas first”Endpoints reference your domain types, so start there. Define them with Schema so they decode and encode automatically.
import { Schema } from "effect"
// A branded id so a UserId can never be confused with a plain numberexport 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:
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).
Endpoints
Section titled “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.
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
Section titled “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. |
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
Section titled “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
Section titled “Empty responses and status codes”HttpApiSchema controls encoding and status:
import { HttpApiSchema } from "effect/unstable/httpapi"import { Schema } from "effect"
HttpApiSchema.NoContent // 204, no bodyHttpApiSchema.Created // 201, no bodyHttpApiSchema.Accepted // 202, no body
// Force a status code onto any schemaconst 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 below.
Grouping endpoints
Section titled “Grouping endpoints”HttpApiGroup.make bundles endpoints. Groups support .prefix(...) to add a
common path segment, .middleware(...) to apply middleware
to every endpoint, and .annotateMerge(...) for OpenAPI metadata.
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" })){}Top-level groups
Section titled “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:
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
Section titled “The root API”HttpApi.make creates the root. Add groups with .add(...), and annotate the
whole API with OpenAPI metadata:
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 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
Section titled “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
Section titled “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(...).
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"The endpoint options object
Section titled “The endpoint options object”The optional third argument fully describes the route. All fields are optional:
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.
Creates the method-specific constructor used by get/post/etc. Use it
directly only when you need a method the helpers do not cover.
import { HttpApiEndpoint } from "effect/unstable/httpapi"
const trace = HttpApiEndpoint.make("TRACE")const ep = trace("trace", "/debug")ep.method // => "TRACE"isHttpApiEndpoint
Section titled “isHttpApiEndpoint”Type guard that narrows an unknown value to an endpoint.
import { HttpApiEndpoint } from "effect/unstable/httpapi"
const ep = HttpApiEndpoint.get("list", "/")HttpApiEndpoint.isHttpApiEndpoint(ep) // => trueHttpApiEndpoint.isHttpApiEndpoint({}) // => false.prefix
Section titled “.prefix”Returns a new endpoint with a path prefix prepended.
import { HttpApiEndpoint } from "effect/unstable/httpapi"
const ep = HttpApiEndpoint.get("list", "/").prefix("/users")ep.path // => "/users/".middleware
Section titled “.middleware”Attaches an HttpApiMiddleware tag to a single
endpoint, merging its required services and declared errors into the endpoint.
// declare a middleware tag elsewhere, then:// HttpApiEndpoint.get("list", "/").middleware(Authorization).annotate / .annotateMerge
Section titled “.annotate / .annotateMerge”Attach annotations (e.g. OpenAPI metadata) to a single endpoint. annotate adds
one Context.Key/value pair; annotateMerge merges an entire Context.
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) // => trueHttpApiSchema reference
Section titled “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
Section titled “NoContent”A Schema.Void annotated with status 204. The default success schema.
import { HttpApiSchema } from "effect/unstable/httpapi"
HttpApiSchema.NoContent // => Schema.Void with httpApiStatus 204Created
Section titled “Created”A Schema.Void annotated with status 201.
import { HttpApiSchema } from "effect/unstable/httpapi"
HttpApiSchema.Created // => Schema.Void with httpApiStatus 201Accepted
Section titled “Accepted”A Schema.Void annotated with status 202.
import { HttpApiSchema } from "effect/unstable/httpapi"
HttpApiSchema.Accepted // => Schema.Void with httpApiStatus 202Creates a Schema.Void with an arbitrary status code — the building block behind
NoContent/Created/Accepted.
import { HttpApiSchema } from "effect/unstable/httpapi"
const Gone = HttpApiSchema.Empty(410) // => Schema.Void with httpApiStatus 410status
Section titled “status”Sets the httpApiStatus annotation on any schema. Accepts a numeric code or a
common status literal (e.g. "Created", "NotFound").
import { Schema } from "effect"import { HttpApiSchema } from "effect/unstable/httpapi"
const A = Schema.String.pipe(HttpApiSchema.status(418)) // => 418const B = Schema.String.pipe(HttpApiSchema.status("Created"))// => 201asText
Section titled “asText”Marks a schema (whose Encoded is a string) as a text/plain payload or
response. Pass contentType to override.
import { Schema } from "effect"import { HttpApiSchema } from "effect/unstable/httpapi"
// encode: value -> "text/plain" body ; decode: body string -> valueSchema.String.pipe(HttpApiSchema.asText())// negotiate a CSV content type while still sending textSchema.String.pipe(HttpApiSchema.asText({ contentType: "text/csv" }))asJson
Section titled “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.
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
Section titled “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.
import { Schema } from "effect"import { HttpApiSchema } from "effect/unstable/httpapi"
// decode: "a=1&b=2" -> { a: "1", b: "2" } ; encode: record -> form bodySchema.Struct({ a: Schema.String, b: Schema.String }).pipe( HttpApiSchema.asFormUrlEncoded())asUint8Array
Section titled “asUint8Array”Marks a schema (whose Encoded is a Uint8Array) as a binary
application/octet-stream payload/response.
import { Schema } from "effect"import { HttpApiSchema } from "effect/unstable/httpapi"
Schema.Uint8Array.pipe(HttpApiSchema.asUint8Array())Schema.Uint8Array.pipe(HttpApiSchema.asUint8Array({ contentType: "image/png" }))asMultipart
Section titled “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.
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 valueasMultipartStream
Section titled “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.
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.PartasNoContent
Section titled “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.
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
Section titled “isNoContent”Predicate over a schema AST; returns true when the schema represents a
no-content (void) response, including transformations whose encoded target is
void.
import { HttpApiSchema } from "effect/unstable/httpapi"
HttpApiSchema.isNoContent(HttpApiSchema.NoContent.ast) // => truegetStatusSuccess / getStatusError
Section titled “getStatusSuccess / getStatusError”Resolve the effective status code for a schema AST. getStatusSuccess defaults
to 200, getStatusError to 500, when no httpApiStatus annotation is
present.
import { Schema } from "effect"import { HttpApiSchema } from "effect/unstable/httpapi"
HttpApiSchema.getStatusSuccess(Schema.String.ast) // => 200HttpApiSchema.getStatusError(Schema.String.ast) // => 500HttpApiSchema.getStatusSuccess(HttpApiSchema.Created.ast) // => 201getPayloadEncoding / getResponseEncoding
Section titled “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.
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
Section titled “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.
import { HttpApiEndpoint, HttpApiError } from "effect/unstable/httpapi"
HttpApiEndpoint.get("getById", "/:id", { error: HttpApiError.NotFound // responds 404})Status error classes
Section titled “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 |
import { HttpApiError } from "effect/unstable/httpapi"
const err = new HttpApiError.Conflict()err._tag // => "Conflict"// HttpApiError.Conflict has httpApiStatus 409
// `BadRequest` ships a reusable singleton instanceHttpApiError.BadRequest.singleton._tag // => "BadRequest"*NoContent variants
Section titled “*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.
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
Section titled “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.
import { HttpApiError } from "effect/unstable/httpapi"
declare const value: unknownif (HttpApiError.HttpApiSchemaError.is(value)) { value.kind // => one of "Params" | "Headers" | "Query" | "Body" | "Payload"}HttpApiGroup reference
Section titled “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.
Creates an empty group with the given identifier. Pass { topLevel: true } to
flatten its endpoints onto the client root.
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 // => falseAdds one or more endpoints. Adding an endpoint whose name already exists
replaces it.
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
Section titled “.prefix”Prepends a path prefix to every endpoint already in the group.
import { HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi"
const g = HttpApiGroup.make("users") .add(HttpApiEndpoint.get("list", "/")) .prefix("/users")g.endpoints.list.path // => "/users/".middleware
Section titled “.middleware”Applies an HttpApiMiddleware to every endpoint
currently in the group. Endpoints added later are not affected.
// HttpApiGroup.make("users").add(...).middleware(Authorization).annotate / .annotateMerge
Section titled “.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.
import { HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
HttpApiGroup.make("users") .annotateMerge(OpenApi.annotations({ title: "Users" }))isHttpApiGroup
Section titled “isHttpApiGroup”Type guard narrowing an unknown value to a group.
import { HttpApiGroup } from "effect/unstable/httpapi"
HttpApiGroup.isHttpApiGroup(HttpApiGroup.make("users")) // => trueHttpApiGroup.isHttpApiGroup({}) // => falseHttpApi reference
Section titled “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).
Creates an empty root API with a stable identifier.
import { HttpApi } from "effect/unstable/httpapi"
const api = HttpApi.make("user-api")api.identifier // => "user-api"Adds one or more groups. Group identifiers are keys — adding a group with the same identifier replaces the previous one.
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
Section titled “.addHttpApi”Merges another HttpApi’s groups into this one, merging the added API’s
annotations into its groups.
import { HttpApi } from "effect/unstable/httpapi"
const a = HttpApi.make("a")const b = HttpApi.make("b")const merged = a.addHttpApi(b).prefix / .middleware
Section titled “.prefix / .middleware”prefix prepends a path to every endpoint in every group currently present;
middleware applies an HttpApiMiddleware to
them. Both only affect groups/endpoints present when called.
import { HttpApi, HttpApiGroup, HttpApiEndpoint } from "effect/unstable/httpapi"
HttpApi.make("user-api") .add(HttpApiGroup.make("users").add(HttpApiEndpoint.get("list", "/"))) .prefix("/api").annotate / .annotateMerge
Section titled “.annotate / .annotateMerge”Attach annotations to the whole API (e.g. OpenAPI title/description).
import { HttpApi, OpenApi } from "effect/unstable/httpapi"
HttpApi.make("user-api") .annotateMerge(OpenApi.annotations({ title: "Acme User API" }))isHttpApi
Section titled “isHttpApi”Type guard narrowing an unknown value to an HttpApi.
import { HttpApi } from "effect/unstable/httpapi"
HttpApi.isHttpApi(HttpApi.make("user-api")) // => trueHttpApi.isHttpApi({}) // => falsereflect
Section titled “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.
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
Section titled “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).
import { Layer, Schema } from "effect"import { HttpApi } from "effect/unstable/httpapi"
const layer = Layer.succeed(HttpApi.AdditionalSchemas, [ Schema.String.annotate({ identifier: "MyString" })])