Skip to content

Routing & Serving

The HttpRouter module is the low-level, hand-written way to build an HTTP server with Effect. You register routes (method + path + handler) into a router Layer, then hand the finished router to a platform server with HttpRouter.serve, or turn it into a Web fetch handler with HttpRouter.toWebHandler.

All of the routing and serving APIs are unstable and import from effect/unstable/http. The host-specific server Layer (NodeHttpServer, BunHttpServer) comes from the platform packages — see Platform.

Route layers do not handle requests when they are constructed. Each HttpRouter.add or HttpRouter.addAll layer registers a Route into the current HttpRouter service while the application Layer is being built. Only when you call serve, toHttpEffect, or toWebHandler is the router read back and turned into an effect that, for each incoming request, finds the matching route and runs its handler.

During a request the router provides these services into the handler’s context:

  • HttpServerRequest — the incoming request.
  • Scope — a request-scoped Scope.
  • HttpServerRequest.ParsedSearchParams — parsed query string.
  • HttpRouter.RouteContext — the matched route and its captured path params.

The common case: define route layers with HttpRouter.add, merge them, and serve.

import { Effect, Layer } from "effect"
import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
// Each route is a Layer that registers itself with the current HttpRouter.
const HelloRoute = HttpRouter.add(
"GET",
"/hello",
// A handler can be a plain HttpServerResponse, an Effect of one, or a
// function from the request to an Effect of one.
Effect.succeed(HttpServerResponse.text("Hello, World!"))
)
const HealthRoute = HttpRouter.add(
"GET",
"/health",
HttpServerResponse.json({ status: "ok" })
)
// Merge the route layers into the application layer.
export const App = Layer.mergeAll(HelloRoute, HealthRoute)

The method is one of "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS", or "*" to match any method. Paths must be absolute (begin with /) or the bare wildcard "*".

  • "/users" — exact match.
  • "/users/:id" — captures a path parameter named id.
  • "/files/*" — wildcard; a path ending in /* also matches the prefix itself (/files matches too).
import { Effect } from "effect"
import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
const UserRoute = HttpRouter.add(
"GET",
"/users/:id",
// The handler form that receives the request directly.
(request) =>
Effect.gen(function* () {
// Read the captured ":id" param from RouteContext.
const { id } = yield* HttpRouter.params
return HttpServerResponse.text(`user ${id} from ${request.url}`)
})
)

HttpRouter.add / addAll are convenience layers built on HttpRouter.use, which gives you the HttpRouter service so you can add several routes inside one Effect.gen:

import { Effect, Layer } from "effect"
import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
const App = Layer.effectDiscard(
Effect.gen(function* () {
const router = yield* HttpRouter.HttpRouter
yield* router.add("GET", "/", HttpServerResponse.text("home"))
yield* router.add("POST", "/echo", (request) =>
Effect.map(request.text, (body) => HttpServerResponse.text(body))
)
})
)

Use HttpRouter.route to build standalone Route values, then register them all at once with HttpRouter.addAll. You can also pass a { prefix } option to mount the whole batch under a path.

import { Effect } from "effect"
import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
const ApiRoutes = HttpRouter.addAll(
[
HttpRouter.route("GET", "/users", HttpServerResponse.text("list users")),
HttpRouter.route("POST", "/users", HttpServerResponse.text("create user"))
],
{ prefix: "/api" } // -> GET /api/users, POST /api/users
)

Handlers read captured path params, query params, and the request body through the HttpRouter schema helpers. Each decodes with a Schema, so you get parsed, typed values and a Schema.SchemaError (mapped to a 400) when input is invalid.

import { Effect, Schema } from "effect"
import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
// :id is a string in the URL; Schema coerces and validates it as a number.
const GetUser = HttpRouter.add(
"GET",
"/users/:id",
Effect.gen(function* () {
const { id } = yield* HttpRouter.schemaPathParams(
Schema.Struct({ id: Schema.FiniteFromString })
)
// HttpServerResponse.json returns an Effect (it can fail to serialize),
// so yield* it to get the response.
return yield* HttpServerResponse.json({ id })
})
)
  • params — raw Record<string, string | undefined> of path params (no decoding).
  • schemaPathParams — decode the path params only.
  • schemaParams — decode path and search params merged together (path wins on a key clash).
  • schemaNoBody — decode method, url, headers, cookies, path params, and search params, without reading the body.
  • schemaJson — decode all of the above plus the parsed JSON body.
import { Effect, Schema } from "effect"
import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
const CreateUser = HttpRouter.add(
"POST",
"/users",
Effect.gen(function* () {
// Pull the typed JSON body out of the request.
const decoded = yield* HttpRouter.schemaJson(
Schema.Struct({
body: Schema.Struct({
name: Schema.String,
age: Schema.Number
})
})
)
return yield* HttpServerResponse.json({ created: decoded.body.name })
})
)

Mount a group of routes under a shared path. addAll(..., { prefix }) is the common path; under the hood it uses router.prefixed, prefixPath, and prefixRoute.

import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
// Mount each batch under its own prefix and merge.
const v1 = HttpRouter.addAll(
[HttpRouter.route("GET", "/ping", HttpServerResponse.text("v1 pong"))],
{ prefix: "/v1" }
)
const v2 = HttpRouter.addAll(
[HttpRouter.route("GET", "/ping", HttpServerResponse.text("v2 pong"))],
{ prefix: "/v2" }
)

Router middleware wraps the response effect of the routes it is provided to. Unlike the serve middleware (which wraps the whole server chain and cannot change the sent response), router middleware can provide services to handlers, handle configured errors, and modify the response.

import { Context, Effect, Layer } from "effect"
import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
class CurrentSession extends Context.Service<CurrentSession, {
readonly token: string
}>()("CurrentSession") {}
// Middleware that provides CurrentSession to the routes it wraps.
const SessionMiddleware = HttpRouter.middleware<{
provides: CurrentSession
}>()(
Effect.succeed((httpEffect) =>
Effect.provideService(httpEffect, CurrentSession, { token: "dummy-token" })
)
).layer
const Route = HttpRouter.add(
"GET",
"/me",
Effect.gen(function* () {
const session = yield* CurrentSession
return HttpServerResponse.text(`token: ${session.token}`)
})
).pipe(Layer.provide(SessionMiddleware))

Pass { global: true } to middleware to apply it to every route in the router. The two most common globals have dedicated helpers: HttpRouter.cors and HttpRouter.disableLogger.

import { HttpRouter } from "effect/unstable/http"
// Apply CORS to all routes.
const Cors = HttpRouter.cors({ allowedOrigins: ["https://example.com"] })

provideRequest — provide a Layer per request

Section titled “provideRequest — provide a Layer per request”

HttpRouter.provideRequest adapts a normal Layer so its services are provided to the routes during request handling, satisfying their request-level requirements.

import { Context, Effect, Layer } from "effect"
import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
class Db extends Context.Service<Db, { readonly url: string }>()("Db") {}
const DbLive = Layer.succeed(Db)({ url: "postgres://localhost" })
const Route = HttpRouter.add(
"GET",
"/db",
Effect.map(Db, (db) => HttpServerResponse.text(db.url))
).pipe(HttpRouter.provideRequest(DbLive))

HttpRouter.serve turns the application Layer into a server Layer. It still requires the HttpServer service, which is provided by a host-specific platform layer. Provide NodeHttpServer.layer or BunHttpServer.layer and launch it.

import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
import { Layer } from "effect"
import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
import { createServer } from "node:http"
const App = HttpRouter.add(
"GET",
"/",
HttpServerResponse.text("Hello from Node!")
)
const ServerLayer = HttpRouter.serve(App).pipe(
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
)
Layer.launch(ServerLayer).pipe(NodeRuntime.runMain)

By default serve installs the request logger and logs the listening address. Disable those with the disableLogger and disableListenLog options. The middleware option wraps the whole server chain.

For serverless / edge runtimes, HttpRouter.toWebHandler builds a (request: Request) => Promise<Response> handler plus a dispose function for the layer scope. Provide HttpServer.layerServices so the platform-neutral services (HttpPlatform, Path, ETag, no-op FileSystem) are available.

import { Layer } from "effect"
import { HttpRouter, HttpServer, HttpServerResponse } from "effect/unstable/http"
const App = HttpRouter.add(
"GET",
"/",
HttpServerResponse.text("Hello from fetch!")
).pipe(Layer.provide(HttpServer.layerServices))
export const { handler, dispose } = HttpRouter.toWebHandler(App)
// const response = await handler(new Request("http://localhost/"))
// => Response with body "Hello from fetch!"

The HttpRouter module, exhaustively. Import from effect/unstable/http.

The service tag (a Context.Service) for the router being assembled. Use yield* HttpRouter.HttpRouter inside a layer effect to get the router and call add / addAll / prefixed on it.

import { Effect } from "effect"
import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
const useRouter = Effect.gen(function* () {
const router = yield* HttpRouter.HttpRouter
yield* router.add("GET", "/", HttpServerResponse.text("ok"))
})

Effect that constructs a fresh, empty HttpRouter service. Used internally by layer; you rarely call it directly.

import { HttpRouter } from "effect/unstable/http"
const router = HttpRouter.make
// => Effect<HttpRouter, never, RouterConfig>

A Layer that provides a newly constructed HttpRouter. serve and toWebHandler include it for you.

import { HttpRouter } from "effect/unstable/http"
const RouterLayer = HttpRouter.layer
// => Layer<HttpRouter>

Builds a standalone Route value from a method, path, and handler (without registering it). Feed an array of routes to addAll.

import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
const r = HttpRouter.route("GET", "/ping", HttpServerResponse.text("pong"))
// => Route<never, never>

Layer that registers a single route. Accepts a static response, an Effect of a response, or a function from the request to an Effect of a response. Optional { uninterruptible } keeps the handler from being made interruptible.

import { Effect } from "effect"
import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
const Route = HttpRouter.add(
"GET",
"/hello",
Effect.succeed(HttpServerResponse.text("Hello, World!"))
)
// => Layer<never, never, HttpRouter | ...>

Layer that registers many routes at once. Accepts an array of Route (or an Effect that produces one) and an optional { prefix } that mounts them all under a path.

import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
const Routes = HttpRouter.addAll(
[HttpRouter.route("GET", "/a", HttpServerResponse.text("a"))],
{ prefix: "/v1" }
)
// => registers GET /v1/a

Builds a route-registration Layer from a function that receives the HttpRouter service. The building block behind add and addAll.

import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
const Route = HttpRouter.use((router) =>
router.add("GET", "/", HttpServerResponse.text("home"))
)
// => Layer<never, never, HttpRouter>

Creates route-scoped (default) or global ({ global: true }) middleware. The type parameters provides / handles declare which services the middleware adds to handlers and which errors it handles. .layer is the layer you provide to routes; .combine composes two middlewares.

import { Effect } from "effect"
import { HttpMiddleware, HttpRouter } from "effect/unstable/http"
// Wrap CORS as router middleware.
const Cors = HttpRouter.middleware(HttpMiddleware.cors()).layer
// => Layer<...> to Layer.provide onto routes

Pure helper that prefixes a path string. Trailing slashes are trimmed from the prefix; "/" becomes the prefix itself and "*" becomes a wildcard under the prefix. Dual.

import { HttpRouter } from "effect/unstable/http"
HttpRouter.prefixPath("/users", "/api") // => "/api/users"
HttpRouter.prefixPath("*", "/api") // => "/api/*"

Returns a copy of a Route with its path prefixed, also tracking the prefix so it can be stripped from the request URL at handle time. Dual.

import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
const base = HttpRouter.route("GET", "/ping", HttpServerResponse.text("pong"))
const prefixed = HttpRouter.prefixRoute(base, "/api")
// => Route matching GET /api/ping

Adapts a Layer so its services are provided to routes during request handling, satisfying their request-level requirements.

import { Context, Effect, Layer } from "effect"
import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
class Db extends Context.Service<Db, { readonly url: string }>()("Db") {}
const Route = HttpRouter.add("GET", "/db", Effect.map(Db, (db) =>
HttpServerResponse.text(db.url)
)).pipe(HttpRouter.provideRequest(Layer.succeed(Db)({ url: "..." })))

Global CORS middleware layer. Options: allowedOrigins, allowedMethods, allowedHeaders, exposedHeaders, maxAge, credentials.

import { HttpRouter } from "effect/unstable/http"
const Cors = HttpRouter.cors({
allowedOrigins: ["https://example.com"],
credentials: true
})
// => Layer<never, never, HttpRouter>

A middleware layer that disables the request logger for the routes it is provided to.

import { Layer } from "effect"
import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
const Route = HttpRouter.add(
"GET",
"/quiet",
HttpServerResponse.text("shh")
).pipe(Layer.provide(HttpRouter.disableLogger))

Effect returning the raw captured path params for the current matched route.

import { HttpRouter } from "effect/unstable/http"
const eff = HttpRouter.params
// => Effect<ReadonlyRecord<string, string | undefined>, never, RouteContext>
// for GET /users/:id matching /users/42 => { id: "42" }

Decodes a Schema from the merged search params and path params (path wins on a clash).

import { Schema } from "effect"
import { HttpRouter } from "effect/unstable/http"
const decode = HttpRouter.schemaParams(
Schema.Struct({ id: Schema.FiniteFromString, q: Schema.optional(Schema.String) })
)
// for /users/42?q=hi => { id: 42, q: "hi" }

Decodes a Schema from the path params only.

import { Schema } from "effect"
import { HttpRouter } from "effect/unstable/http"
const decode = HttpRouter.schemaPathParams(
Schema.Struct({ id: Schema.FiniteFromString })
)
// for /users/42 => { id: 42 }

Decodes a Schema from the full request envelope including the parsed JSON body (method, url, headers, cookies, pathParams, searchParams, body). Fails with HttpServerError if the body cannot be parsed, or Schema.SchemaError on decode failure.

import { Schema } from "effect"
import { HttpRouter } from "effect/unstable/http"
const decode = HttpRouter.schemaJson(
Schema.Struct({ body: Schema.Struct({ name: Schema.String }) })
)
// for POST {"name":"Ada"} => { body: { name: "Ada" } }

Like schemaJson but never reads the request body — decodes method, url, headers, cookies, pathParams, and searchParams.

import { Schema } from "effect"
import { HttpRouter } from "effect/unstable/http"
const decode = HttpRouter.schemaNoBody(
Schema.Struct({ headers: Schema.Struct({ "x-trace": Schema.String }) })
)
// reads the x-trace header without consuming the body

The Context.Service holding the matched route and its captured params. The schema helpers and params read from it; you can also access it directly.

import { Effect } from "effect"
import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
const Route = HttpRouter.add("GET", "/u/:id", Effect.gen(function* () {
const ctx = yield* HttpRouter.RouteContext
return HttpServerResponse.text(`${ctx.route.path} -> ${ctx.params.id}`)
// => "/u/:id -> 7" for /u/7
}))

A Context.Reference for low-level route-matcher configuration, defaulting to {}. Override it via the routerConfig option on serve / toWebHandler, or by providing it as a layer.

import { HttpRouter } from "effect/unstable/http"
const config = HttpRouter.RouterConfig
// => Context.Reference<Partial<FindMyWay.RouterConfig>>

Turns an application Layer into a server Layer. Requires HttpServer (provided by a platform layer). Options: routerConfig, disableLogger, disableListenLog, and middleware (wraps the whole server chain).

import { NodeHttpServer } from "@effect/platform-node"
import { Layer } from "effect"
import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
import { createServer } from "node:http"
const App = HttpRouter.add("GET", "/", HttpServerResponse.text("ok"))
const ServerLayer = HttpRouter.serve(App, { disableListenLog: true }).pipe(
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
)

Builds the application Layer and returns the router as a single handler Effect (HttpServerResponse from HttpServerRequest + Scope). Useful for embedding the router into a larger server effect.

import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
const App = HttpRouter.add("GET", "/", HttpServerResponse.text("ok"))
const handlerEffect = HttpRouter.toHttpEffect(App)
// => Effect<Effect<HttpServerResponse, ..., Scope | HttpServerRequest | ...>, ...>

Builds a Web fetch-compatible handler ((request: Request) => Promise<Response>) plus a dispose function. Options: memoMap, routerConfig, disableLogger, middleware.

import { Layer } from "effect"
import { HttpRouter, HttpServer, HttpServerResponse } from "effect/unstable/http"
const App = HttpRouter.add("GET", "/", HttpServerResponse.text("ok")).pipe(
Layer.provide(HttpServer.layerServices)
)
export const { handler, dispose } = HttpRouter.toWebHandler(App)
// const res = await handler(new Request("http://x/")) // => 200 "ok"

Route<E, R> is the description of a registered route (method, path, handler, prefix, interruptibility). Route.Error<R> and Route.Context<T> extract a route’s error and requirement types at the type level.

import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
const r = HttpRouter.route("GET", "/", HttpServerResponse.text("x"))
type E = HttpRouter.Route.Error<typeof r> // => never
type R = HttpRouter.Route.Context<typeof r> // => never

The path type accepted by the router: `/${string}` or "*".

import { HttpRouter } from "effect/unstable/http"
const path: HttpRouter.PathInput = "/users/:id"

The runtime that actually listens for connections and supplies HttpServerRequest per request. Platform packages implement it; you typically consume it through HttpRouter.serve.

The service tag. Exposes serve (run a response effect per request) and address (where it is listening).

import { Effect } from "effect"
import { HttpServer } from "effect/unstable/http"
const addr = Effect.map(HttpServer.HttpServer, (s) => s.address)
// => Effect<Address, never, HttpServer>

Constructs an HttpServer service from a serve implementation and an address. Used by platform adapters.

import { Effect } from "effect"
import { HttpServer } from "effect/unstable/http"
const service = HttpServer.make({
serve: (_httpEffect, _middleware) => Effect.void,
address: { _tag: "TcpAddress", hostname: "127.0.0.1", port: 3000 }
})

Creates a Layer that starts serving a response effect with the current HttpServer. The request service is supplied by the server; an optional middleware is applied at the server boundary. (HttpRouter.serve wraps this for router layers.)

import { HttpServer, HttpServerResponse } from "effect/unstable/http"
const ServeLayer = HttpServer.serve(
HttpServerResponse.text("hello")
)
// => Layer<never, never, HttpServer | ...>

Like serve but returns an Effect (requiring Scope) instead of a Layer.

import { HttpServer, HttpServerResponse } from "effect/unstable/http"
const eff = HttpServer.serveEffect(HttpServerResponse.text("hi"))
// => Effect<void, never, Scope | HttpServer | ...>

Formats an Address as a display string: TCP becomes http://host:port, Unix becomes unix://path.

import { HttpServer } from "effect/unstable/http"
HttpServer.formatAddress({ _tag: "TcpAddress", hostname: "localhost", port: 3000 })
// => "http://localhost:3000"

Reads the current server address, formats it, and passes the string to your effectful function.

import { Effect } from "effect"
import { HttpServer } from "effect/unstable/http"
const log = HttpServer.addressFormattedWith((url) => Effect.log(`up at ${url}`))
// => Effect<void, never, HttpServer>

Effect that logs Listening on <formatted address> for the current server.

import { HttpServer } from "effect/unstable/http"
const eff = HttpServer.logAddress
// => logs e.g. "Listening on http://localhost:3000"

Wraps a server-providing Layer so it logs the listening address on startup. serve applies this by default unless disableListenLog is set.

import { NodeHttpServer } from "@effect/platform-node"
import { HttpServer } from "effect/unstable/http"
import { createServer } from "node:http"
const ServerLayer = HttpServer.withLogAddress(
NodeHttpServer.layer(createServer, { port: 3000 })
)

Builds an HttpClient whose requests are prepended with the running server’s URL (and 0.0.0.0 rewritten to 127.0.0.1). TCP only — Unix sockets are not supported.

import { HttpServer } from "effect/unstable/http"
const clientEffect = HttpServer.makeTestClient
// => Effect<HttpClient, never, HttpServer | HttpClient>

Layer that provides the test HttpClient from makeTestClient.

import { HttpServer } from "effect/unstable/http"
const TestClient = HttpServer.layerTestClient
// => Layer<HttpClient, never, HttpServer | HttpClient>

Layer bundling the platform-neutral services HTTP servers commonly need: HttpPlatform, Path, a weak ETag generator, and a no-op FileSystem. Provide it to toWebHandler apps.

import { HttpServer } from "effect/unstable/http"
const Services = HttpServer.layerServices
// => Layer<Path | HttpPlatform | FileSystem | Etag.Generator>

Address is the union TcpAddress | UnixAddress. TcpAddress has hostname and port; UnixAddress has path. Both carry a _tag discriminator.

import type { HttpServer } from "effect/unstable/http"
const tcp: HttpServer.TcpAddress = { _tag: "TcpAddress", hostname: "0.0.0.0", port: 3000 }
const unix: HttpServer.UnixAddress = { _tag: "UnixAddress", path: "/tmp/app.sock" }

HttpRouter.toWebHandler delegates to the lower-level HttpEffect module, which you can use directly to bridge Effect HTTP programs and Web-standard Request / Response. Import from effect/unstable/http.

Converts an HTTP server effect (requiring only HttpServerRequest | Scope) into a (request: Request) => Promise<Response> handler with an empty base context.

import { Effect } from "effect"
import { HttpEffect, HttpServerResponse } from "effect/unstable/http"
const app = Effect.succeed(HttpServerResponse.text("ok"))
const handler = HttpEffect.toWebHandler(app)
// const res = await handler(new Request("http://x/")) // => 200 "ok"

Same as toWebHandler but you supply a base Context whose services are available to every request.

import { Context, Effect } from "effect"
import { HttpEffect, HttpServerResponse } from "effect/unstable/http"
const handlerFactory = HttpEffect.toWebHandlerWith(Context.empty())
const handler = handlerFactory(Effect.succeed(HttpServerResponse.text("ok")))

Builds a web handler whose services come from a Layer, returning { handler, dispose }. Call dispose to release the layer scope at shutdown.

import { Effect } from "effect"
import { HttpEffect, HttpServer, HttpServerResponse } from "effect/unstable/http"
const { handler, dispose } = HttpEffect.toWebHandlerLayer(
Effect.succeed(HttpServerResponse.text("ok")),
HttpServer.layerServices
)

The most general form: a Layer plus a toHandler factory that builds the response effect from the layer’s context. HttpRouter.toWebHandler is implemented with this.

import { Effect } from "effect"
import { HttpEffect, HttpServer, HttpServerResponse } from "effect/unstable/http"
const { handler, dispose } = HttpEffect.toWebHandlerLayerWith(
HttpServer.layerServices,
{ toHandler: () => Effect.succeed(Effect.succeed(HttpServerResponse.text("ok"))) }
)

The inverse: embeds an existing Web-standard handler inside an Effect HTTP server, producing an HttpServerResponse for the current HttpServerRequest.

import { HttpEffect } from "effect/unstable/http"
const eff = HttpEffect.fromWebHandler(
async (_req) => new Response("from web handler")
)
// => Effect<HttpServerResponse, HttpServerError, HttpServerRequest>

The type of a function run with the current request and response just before the response is sent; it may replace the response or fail with HttpServerError.

import { Effect } from "effect"
import type { HttpEffect } from "effect/unstable/http"
import { HttpServerResponse } from "effect/unstable/http"
const handler: HttpEffect.PreResponseHandler = (_request, response) =>
Effect.succeed(HttpServerResponse.setHeader(response, "x-served-by", "effect"))

Registers a pre-response handler for the current request. Runs before the bytes are sent (not after a streaming body starts).

import { Effect } from "effect"
import { HttpEffect, HttpServerResponse } from "effect/unstable/http"
const eff = HttpEffect.appendPreResponseHandler((_req, res) =>
Effect.succeed(HttpServerResponse.setHeader(res, "x-app", "demo"))
)
// => Effect<void, never, HttpServerRequest>

Runs an effect after registering a pre-response handler for the current request. Dual.

import { Effect } from "effect"
import { HttpEffect, HttpServerResponse } from "effect/unstable/http"
const eff = HttpEffect.withPreResponseHandler(
Effect.succeed(HttpServerResponse.text("ok")),
(_req, res) => Effect.succeed(HttpServerResponse.setHeader(res, "x-app", "demo"))
)

Failures raised by the server runtime live in HttpServerError. Import from effect/unstable/http.

The wrapper tagged error (_tag: "HttpServerError"). It carries a reason and exposes request and optional response, and is Respondable so it can convert itself to an HTTP response.

import { HttpServerError } from "effect/unstable/http"
declare const u: unknown
if (HttpServerError.isHttpServerError(u)) {
// u.reason, u.request, u.response
}

Refinement predicate for HttpServerError.

import { HttpServerError } from "effect/unstable/http"
HttpServerError.isHttpServerError(new Error("x")) // => false

Reason raised when no route matched. Converts to an empty 404 and is ignored by the error reporter.

import { HttpServerError } from "effect/unstable/http"
declare const request: any
const reason = new HttpServerError.RouteNotFound({ request })
// => 404

Reason for a failure to parse or read the incoming request. Converts to an empty 400.

import { HttpServerError } from "effect/unstable/http"
declare const request: any
const reason = new HttpServerError.RequestParseError({ request, description: "bad body" })
// => 400

Reason tied to a response that was being built or sent; carries both request and response. Converts to an empty 500 (it does not reuse the failed response, which may be partly sent).

import { HttpServerError } from "effect/unstable/http"
declare const request: any
declare const response: any
const reason = new HttpServerError.ResponseError({ request, response })
// => 500

Reason for an unexpected server-side failure while handling a request. Converts to an empty 500.

import { HttpServerError } from "effect/unstable/http"
declare const request: any
const reason = new HttpServerError.InternalError({ request, cause: new Error("boom") })
// => 500

Tagged error wrapping a low-level failure from the server implementation itself (outside an individual handler response) — e.g. the port could not be bound.

import { HttpServerError } from "effect/unstable/http"
const err = new HttpServerError.ServeError({ cause: new Error("EADDRINUSE") })

A context annotation marking an interrupt as caused by the client aborting the request. causeResponse maps it to a 499 instead of a server-abort 503.

import { HttpServerError } from "effect/unstable/http"
const annotation = HttpServerError.ClientAbort.annotation
// used to annotate an interrupt => maps to 499

Type aliases: RequestError = RequestParseError | RouteNotFound | InternalError, and HttpServerErrorReason = RequestError | ResponseError — the value carried as the reason of an HttpServerError.

import type { HttpServerError } from "effect/unstable/http"
declare const reason: HttpServerError.HttpServerErrorReason

Converts a failed handler Cause into the [response, cause] pair to send and report. Respondable failures choose their own response; pure interrupts become 499 (client abort) or 503 (server abort); everything else defaults to 500.

import { Cause } from "effect"
import { HttpServerError } from "effect/unstable/http"
const eff = HttpServerError.causeResponse(Cause.die("boom"))
// => Effect<[HttpServerResponse (500), Cause]>

Extracts the HttpServerResponse from a successful handler Exit, or derives one (stripping any embedded response defect, else 500) from the failure cause.

import { Exit } from "effect"
import { HttpServerError, HttpServerResponse } from "effect/unstable/http"
const res = HttpServerError.exitResponse(Exit.succeed(HttpServerResponse.text("ok")))
// => HttpServerResponse "ok"

  • HttpApi — schema-first APIs with typed clients and OpenAPI, built on this router.
  • HTTP Client — make outbound requests (and test your server).
  • Platform — the host adapters (NodeHttpServer, BunHttpServer) that provide the HttpServer service.