Skip to content

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.

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 server Layer you run with a platform HttpServer.
  • HttpRouter.toWebHandler(appLayer) — produces a (request: Request) => Promise<Response> Fetch handler plus a dispose function.
  • 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.

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 resources

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")))
})
)
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"
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 })
})
)
import { HttpServerResponse } from "effect/unstable/http"
HttpServerResponse.text("hello") // => 200 text/plain
HttpServerResponse.json({ ok: true }) // => Effect -> 200 {"ok":true} (stringify may fail)
HttpServerResponse.empty({ status: 201 }) // => 201 with no body
HttpServerResponse.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; HttpOnly

Add 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 depth
  • Routing — define routes, HTTP methods, path params, prefixes, and schema-decoded inputs with HttpRouter; serve on a platform server or as a Web fetch handler. Full HttpRouter, 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, and Cookies reference.
  • Server middleware — wrap handlers for CORS, logging, tracing, and request-scoped services with HttpMiddleware and HttpRouter.middleware.
  • Static files — serve directories and one-off files from disk, generate ETags, and receive multipart uploads.