Serving & clients
With the API defined and the handlers implemented, two things are derived from
the same Api value: a server that mounts the routes and a typed client
whose methods mirror the endpoints. This page wires both.
Assembling the server
Section titled “Assembling the server”HttpApiBuilder.layer(Api) turns the API plus its handler groups into routes.
Provide each group’s handler Layer, then serve the result with a platform HTTP
server. The example also mounts interactive docs and
exposes the OpenAPI JSON.
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"import { Layer } from "effect"import { HttpRouter } from "effect/unstable/http"import { HttpApiBuilder, HttpApiScalar } from "effect/unstable/httpapi"import { createServer } from "node:http"import { Api } from "../api/Api.ts"import { AuthorizationLayer } from "./Authorization.ts"import { SystemApiHandlers } from "./System/http.ts"import { Users } from "./Users.ts"import { UsersApiHandlers } from "./Users/http.ts"
// Build the API routes and serve the OpenAPI document at /openapi.jsonconst ApiRoutes = HttpApiBuilder.layer(Api, { openapiPath: "/openapi.json"}).pipe( // Provide every group's handler Layer (and their dependencies) Layer.provide([ UsersApiHandlers.pipe(Layer.provide([Users.layer, AuthorizationLayer])), SystemApiHandlers ]))
// Serve interactive Scalar docs at /docsconst DocsRoute = HttpApiScalar.layer(Api, { path: "/docs" })
// Combine all the route layersconst AllRoutes = Layer.mergeAll(ApiRoutes, DocsRoute)
// Mount the routes on a Node HTTP server listening on port 3000const HttpServerLayer = HttpRouter.serve(AllRoutes).pipe( Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })))
// Launch and run. Layer.launch keeps the server alive until interrupted.Layer.launch(HttpServerLayer).pipe(NodeRuntime.runMain)Serverless / Web standard handler
Section titled “Serverless / Web standard handler”For environments that speak the Fetch API (edge functions, workers), produce a
(request: Request) => Promise<Response> handler instead of binding a port:
import { Layer } from "effect"import { HttpRouter, HttpServer } from "effect/unstable/http"
// `layerServices` supplies the platform services a router needs without a serverexport const { handler, dispose } = HttpRouter.toWebHandler( AllRoutes.pipe(Layer.provide(HttpServer.layerServices)))The typed client
Section titled “The typed client”HttpApiClient.make(Api) builds a client whose shape exactly follows the API:
groups become namespaces, endpoints become methods, and topLevel groups attach
their methods to the root. Inputs and outputs are typed and validated against the
same schemas the server uses.
import { Context, flow, Layer, Schedule } from "effect"import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"import { HttpApiClient } from "effect/unstable/httpapi"import { Api } from "../api/Api.ts"import { AuthorizationClient } from "./AuthorizationClient.ts"
export class ApiClient extends Context.Service< ApiClient, // ForApi derives the full client interface from the API definition HttpApiClient.ForApi<typeof Api>>()("acme/ApiClient") { static readonly layer = Layer.effect( ApiClient, HttpApiClient.make(Api, { // transformClient adjusts the underlying HttpClient: set the base URL, // add retries, attach default headers, etc. transformClient: (client) => client.pipe( HttpClient.mapRequest(flow( HttpClientRequest.prependUrl("http://localhost:3000") )), HttpClient.retryTransient({ schedule: Schedule.exponential(100), times: 3 }) ) }) ).pipe( // Provide the client implementation of any `requiredForClient` middleware Layer.provide(AuthorizationClient), // Provide an HttpClient backend. FetchHttpClient works in the browser and // edge runtimes; NodeHttpClient / BunHttpClient are also available. Layer.provide(FetchHttpClient.layer) )}Calling the API is now just calling methods. The compiler enforces argument shapes and infers the result and error types from the endpoint definitions:
import { Effect } from "effect"import { UserId } from "../domain/User.ts"import { ApiClient } from "./ApiClient.ts"
export const program = Effect.gen(function*() { const client = yield* ApiClient
// topLevel "system" group -> method on the root yield* client.health()
// "users" group -> client.users.<endpoint>. Each method takes one object // whose keys (params / query / payload / headers) mirror the endpoint inputs. // `id` decodes to the branded UserId, so build it with UserId.make. const user = yield* client.users.getById({ params: { id: UserId.make(1) } }) const created = yield* client.users.create({ payload: { name: "Ada", email: "ada@acme.dev" } }) const all = yield* client.users.list({ query: { search: "ada" } })
return { user, created, all }}).pipe(Effect.provide(ApiClient.layer))Testing without a network
Section titled “Testing without a network”HttpApiTest builds an in-memory client that drives your real handlers
through the same encode/decode/routing pipeline as a live server — no port, no
sockets. Provide the handler layers for the groups under test, then call the
client just like the real one. See Testing for the broader testing
story.
The next step is publishing the contract as OpenAPI.