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.
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 }),
// 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 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 GET/HEAD | JSON by default. |
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.
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.
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.