HTTP Server
The HTTP server modules are the low-level primitives for serving HTTP in Effect.
You describe routes as values with HttpRouter, return immutable
HttpServerResponse values from handlers, and run the whole thing on a concrete
HttpServer provided by a platform adapter (Node, Bun, Deno) — or convert it to a
Fetch-compatible web handler for serverless environments.
These same primitives power the higher-level HTTP API layer:
HttpApiBuilder ultimately registers routes into an HttpRouter and serves them
through an HttpServer. Reach for HttpApi when you want a schema-first,
fully-typed API with a generated client; reach for these primitives directly when
you want raw routing control, webhooks, health checks, file serving, or a custom
Fetch handler.
Mental model
Section titled “Mental model”An HttpRouter is built up through layers: route layers (HttpRouter.add,
HttpRouter.addAll, HttpRouter.use) register Route values into the current
router while the application Layer is being constructed. Nothing handles a
request immediately. Then one of three entry points reads the completed router and
runs the matching handler per request:
HttpRouter.serve(appLayer)— produces a serverLayeryou run with a platformHttpServer.HttpRouter.toWebHandler(appLayer)— produces a(request: Request) => Promise<Response>Fetch handler plus adisposefunction.HttpRouter.toHttpEffect(appLayer)— produces the raw per-request handler effect.
During a request the router provides HttpServerRequest, a Scope, parsed search
parameters, and RouteContext (captured path params), so handlers can decode the
request and access services supplied by middleware.
A complete minimal example
Section titled “A complete minimal example”This serves two routes on Node. Notice that routes are Layer values merged
together, and the response constructors (HttpServerResponse.text / .json) are
plain immutable values — json returns an Effect because serialization can fail.
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"import { Effect, Layer } from "effect"import { HttpRouter, HttpServerResponse } from "effect/unstable/http"import { createServer } from "node:http"
// A static-response route: the handler is just an Effect that produces a response.const HelloRoute = HttpRouter.add( "GET", "/hello", Effect.succeed(HttpServerResponse.text("Hello, World!")))
// A route whose handler reads the matched path parameters via RouteContext.const GreetRoute = HttpRouter.add( "GET", "/greet/:name", Effect.gen(function* () { const params = yield* HttpRouter.params // HttpServerResponse.json returns an Effect (JSON.stringify may throw) return yield* HttpServerResponse.json({ hello: params.name }) }))
// Merge the route layers into the application layer.const AppLayer = Layer.mergeAll(HelloRoute, GreetRoute)
// Turn the app layer into a server Layer, then provide a platform HttpServer.const ServerLayer = HttpRouter.serve(AppLayer).pipe( Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })))
// Launch it. `HttpRouter.serve` already logs the listening address.Layer.launch(ServerLayer).pipe(NodeRuntime.runMain)// => GET http://localhost:3000/hello -> "Hello, World!"// => GET http://localhost:3000/greet/Alice -> {"hello":"Alice"}The exact same AppLayer can become a serverless Fetch handler instead of a
listening server:
import { HttpRouter, HttpServer } from "effect/unstable/http"
const { handler, dispose } = HttpRouter.toWebHandler( // toWebHandler needs the HTTP platform services (Path, FileSystem, etc.) AppLayer.pipe(Layer.provide(HttpServer.layerServices)))
// handler: (request: Request) => Promise<Response>const response = await handler(new Request("http://localhost/hello"))// => Response with body "Hello, World!"
await dispose() // release layer resourcesThe grab bag
Section titled “The grab bag”The handful of things you reach for most when building a server. Each one has a full reference on the sub-pages linked below; this is the shortlist.
Register routes — statically or imperatively
Section titled “Register routes — statically or imperatively”import { Effect } from "effect"import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
// One route at a time. The handler is an Effect producing a response.const Ping = HttpRouter.add("GET", "/ping", Effect.succeed(HttpServerResponse.text("pong")))
// Many at once, under a shared prefix.const Api = HttpRouter.addAll([ HttpRouter.route("GET", "/health", Effect.succeed(HttpServerResponse.empty())), HttpRouter.route("GET", "/version", Effect.succeed(HttpServerResponse.text("1.0.0")))], { prefix: "/api" })// => GET /api/health (204), GET /api/version ("1.0.0")
// Imperatively, when registration depends on other services.const Dynamic = HttpRouter.use((router) => Effect.gen(function* () { yield* router.add("GET", "/a", Effect.succeed(HttpServerResponse.text("a"))) yield* router.add("GET", "/b", Effect.succeed(HttpServerResponse.text("b"))) }))Decode path & query params with a Schema
Section titled “Decode path & query params with a Schema”import { Effect, Schema } from "effect"import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
const Params = Schema.Struct({ id: Schema.NumberFromString, page: Schema.NumberFromString })
HttpRouter.add( "GET", "/items/:id", Effect.gen(function* () { // schemaParams decodes path + search params together (path wins on conflict) const { id, page } = yield* HttpRouter.schemaParams(Params) return HttpServerResponse.text(`item ${id} page ${page}`) // both are numbers }))// => GET /items/42?page=2 -> "item 42 page 2"Decode a JSON body with a Schema
Section titled “Decode a JSON body with a Schema”import { Effect, Schema } from "effect"import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
const CreateUser = Schema.Struct({ body: Schema.Struct({ name: Schema.String, age: Schema.Number })})
HttpRouter.add( "POST", "/users", Effect.gen(function* () { // schemaJson decodes the whole request, including the parsed JSON body const { body } = yield* HttpRouter.schemaJson(CreateUser) return yield* HttpServerResponse.json({ created: body.name }) }))Build responses
Section titled “Build responses”import { HttpServerResponse } from "effect/unstable/http"
HttpServerResponse.text("hello") // => 200 text/plainHttpServerResponse.json({ ok: true }) // => Effect -> 200 {"ok":true} (stringify may fail)HttpServerResponse.empty({ status: 201 }) // => 201 with no bodyHttpServerResponse.redirect("/login") // => 302, Location: /login
// Combinators return a NEW response — pipe to layer on status, headers, cookies.HttpServerResponse.text("nope").pipe( HttpServerResponse.setStatus(404), HttpServerResponse.setHeader("x-trace", "abc"), HttpServerResponse.setCookieUnsafe("session", "abc123", { httpOnly: true }))// => 404, header x-trace + Set-Cookie: session=abc123; HttpOnlyAdd CORS, logging, or request-scoped services with middleware
Section titled “Add CORS, logging, or request-scoped services with middleware”import { Context, Effect, Layer } from "effect"import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
// Apply CORS headers to every route.const Cors = HttpRouter.cors({ allowedOrigins: ["https://example.com"], allowedMethods: ["GET", "POST"]})
// Provide a request-scoped service that handlers can then `yield*`.class CurrentUser extends Context.Service<CurrentUser, { id: string }>()("CurrentUser") {}
const WithUser = HttpRouter.middleware<{ provides: CurrentUser }>()( Effect.succeed((httpEffect) => Effect.provideService(httpEffect, CurrentUser, { id: "u1" }) )).layer// => Layers you provide to routes; see /http-server/server-middleware/ for depthIn this section
Section titled “In this section”- Routing — define routes, HTTP methods, path params,
prefixes, and schema-decoded inputs with
HttpRouter; serve on a platform server or as a Webfetchhandler. FullHttpRouter,HttpServer,HttpEffect, and server-error reference. - Request & Response — read the incoming
request (bodies, headers, cookies, search params) and build responses, plus the
HttpBody,Headers,UrlParams,Url, andCookiesreference. - Server middleware — wrap handlers for CORS,
logging, tracing, and request-scoped services with
HttpMiddlewareandHttpRouter.middleware. - Static files — serve directories and one-off files from disk, generate ETags, and receive multipart uploads.