Middleware
HTTP middleware lets you wrap a request handler with cross-cutting behavior:
access logging, trace spans, CORS, proxy-header handling, and search-param
parsing. In Effect v4 server middleware lives in
effect/unstable/http/HttpMiddleware, and HttpRouter provides shortcuts for
the most common cases.
import { Effect, Layer } from "effect"import { HttpMiddleware, HttpRouter, HttpServerResponse} from "effect/unstable/http"
const HelloRoute = HttpRouter.add( "GET", "/hello", Effect.succeed(HttpServerResponse.text("Hello, World!")))
// Apply CORS to every route in the routerconst CorsLayer = HttpRouter.cors()
const App = Layer.mergeAll(HelloRoute, CorsLayer)Mental model
Section titled “Mental model”An HttpMiddleware is just a function from one HTTP handler effect to another:
interface HttpMiddleware { <E, R>( self: Effect.Effect<HttpServerResponse, E, R | HttpServerRequest> ): Effect.Effect<HttpServerResponse, any, any>}Because the handler effect carries the per-request HttpServerRequest in its
context, middleware can inspect or rewrite the request, provide request-scoped
services, attach pre-response hooks, or observe the handler’s exit, all while
preserving normal Effect error and interruption semantics.
There are two places middleware runs, and the distinction matters:
- Router middleware (
HttpRouter.middleware,HttpRouter.cors,HttpRouter.disableLogger) wraps the route handlers. Use this when middleware needs to provide a service, handle a route error, or change the response before it is sent. - Server middleware (the
middlewareoption ofHttpRouter.serve/HttpRouter.toWebHandler) wraps the entire server chain, including the act of sending the response. Changes it makes to the response are not reflected in what the client receives. This is where the accessloggeris installed by default.
Applying middleware
Section titled “Applying middleware”Built-in HttpMiddleware on the server chain
Section titled “Built-in HttpMiddleware on the server chain”HttpRouter.serve installs HttpMiddleware.logger for you unless you pass
disableLogger: true. You can compose additional server-level middleware via
the middleware option:
import { HttpMiddleware, HttpRouter, HttpServer} from "effect/unstable/http"
const ServerLayer = HttpRouter.serve(App, { // wrap the server chain with proxy-header trust + tracing middleware: (httpApp) => httpApp.pipe(HttpMiddleware.xForwardedHeaders, HttpMiddleware.tracer)})Router middleware via HttpRouter.middleware
Section titled “Router middleware via HttpRouter.middleware”HttpRouter.middleware turns an HttpMiddleware (or an effect that builds one)
into a Middleware whose .layer you provide to the routes it should wrap. By
default it affects only the routes it is provided to; pass { global: true }
to apply it to every route.
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* a service to the wrapped handlers.// The `provides` config records what the middleware supplies.const SessionMiddleware = HttpRouter.middleware<{ provides: CurrentSession}>()( Effect.gen(function*() { yield* Effect.log("SessionMiddleware initialized") return (httpEffect) => Effect.provideService(httpEffect, CurrentSession, { token: "dummy-token" }) })).layer
const ProtectedRoute = HttpRouter.add( "GET", "/me", Effect.gen(function*() { const session = yield* CurrentSession // available thanks to the middleware return HttpServerResponse.text(`token: ${session.token}`) })).pipe(Layer.provide(SessionMiddleware))Router shortcuts
Section titled “Router shortcuts”HttpRouter.cors(options?)— installsHttpMiddleware.corsglobally for the router. Returns aLayer<never, never, HttpRouter>.HttpRouter.disableLogger— a ready-made layer that disables the access logger for the routes it is provided to.
import { Effect, Layer } from "effect"import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
const Healthcheck = HttpRouter.add( "GET", "/health", Effect.succeed(HttpServerResponse.text("ok"))).pipe( // don't log the (noisy) healthcheck route Layer.provide(HttpRouter.disableLogger))HttpMiddleware reference
Section titled “HttpMiddleware reference”Every public export of effect/unstable/http/HttpMiddleware.
Defines an HttpMiddleware while preserving its precise type. This is the entry
point for writing your own middleware: take the handler effect, transform it,
and return the new effect.
import { Effect } from "effect"import { HttpMiddleware, HttpServerRequest, HttpServerResponse} from "effect/unstable/http"
// A middleware that adds an `x-powered-by` header by rewriting the response.const poweredBy = HttpMiddleware.make((httpApp) => Effect.map(httpApp, (response) => HttpServerResponse.setHeader(response, "x-powered-by", "Effect") ))
// Use it like any other middleware:const handler = poweredBy( Effect.succeed(HttpServerResponse.text("hi"))) satisfies Effect.Effect< HttpServerResponse.HttpServerResponse, never, HttpServerRequest.HttpServerRequest>A middleware can also read the request via Effect.withFiber /
HttpServerRequest, provide request-scoped services, or branch on the exit.
logger
Section titled “logger”Logs each sent HTTP response with http.method, http.url, and http.status
annotations, inside an http.span log span. On failure it logs the stripped
cause. HttpRouter.serve installs this for you by default.
import { HttpMiddleware, HttpServerResponse } from "effect/unstable/http"
const app = HttpMiddleware.logger( // => logs: "Sent HTTP response" http.method=GET http.url=/ http.status=200 HttpServerResponse.text("ok") as any)withLoggerDisabled
Section titled “withLoggerDisabled”Runs an effect with response logging disabled for the current server
request. Useful inside a specific handler when you don’t want the default
logger to emit a line.
import { Effect } from "effect"import { HttpMiddleware, HttpServerResponse } from "effect/unstable/http"
const quietHandler = HttpMiddleware.withLoggerDisabled( Effect.succeed(HttpServerResponse.text("no log line for this one")))// => the `logger` middleware skips this requestHandles CORS preflight (OPTIONS) requests and appends the configured CORS
headers to responses. Call it with no arguments for permissive defaults
(access-control-allow-origin: * and all common methods), or pass an options
object.
import { HttpMiddleware } from "effect/unstable/http"
const corsMiddleware = HttpMiddleware.cors({ // string[] OR a Predicate<string>; [] means "*" allowedOrigins: ["https://app.example.com"], // default: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"] allowedMethods: ["GET", "POST"], // [] reflects the request's Access-Control-Request-Headers allowedHeaders: ["Content-Type", "Authorization"], // headers the browser is allowed to read off the response exposedHeaders: ["X-Custom"], // sets access-control-allow-credentials: true credentials: true, // access-control-max-age (seconds) for preflight caching maxAge: 600})// OPTIONS requests => 204 with the computed preflight headers// other requests => original response + access-control-* headers appendedThe options object (all fields optional):
| Field | Type | Default | Effect |
|---|---|---|---|
allowedOrigins | ReadonlyArray<string> | Predicate<string> | [] | [] emits access-control-allow-origin: *; one origin pins it (with Vary: Origin); many or a predicate reflects the matching Origin. |
allowedMethods | ReadonlyArray<string> | ["GET","HEAD","PUT","PATCH","POST","DELETE"] | access-control-allow-methods on preflight. |
allowedHeaders | ReadonlyArray<string> | [] | access-control-allow-headers; empty reflects the request’s Access-Control-Request-Headers. |
exposedHeaders | ReadonlyArray<string> | [] | access-control-expose-headers. |
credentials | boolean | false | when true, adds access-control-allow-credentials: true. |
maxAge | number | undefined | access-control-max-age (seconds). |
tracer
Section titled “tracer”Creates a server trace span (kind: "server") for each request and records HTTP
request/response attributes (http.request.method, url.full, url.path,
http.response.status_code, redacted request/response headers, client address,
user agent). The parent span is extracted from the request’s trace-context
headers when present.
import { HttpMiddleware, HttpRouter } from "effect/unstable/http"
const ServerLayer = HttpRouter.serve(App, { middleware: HttpMiddleware.tracer})// each request => a "http.server GET" span with url.*/http.* attributesTracing is skipped when the fiber’s TracerEnabled ref is off, or when
TracerDisabledWhen matches the request.
SpanNameGenerator
Section titled “SpanNameGenerator”A Context.Reference holding the function that names server spans. Defaults to
(request) => `http.server ${request.method}` . Override it via
Layer.succeed.
import { Layer } from "effect"import { HttpMiddleware } from "effect/unstable/http"
const SpanNames = Layer.succeed(HttpMiddleware.SpanNameGenerator)( (request) => `${request.method} ${request.url}`)// spans now named e.g. "GET /users/42"TracerDisabledWhen
Section titled “TracerDisabledWhen”A Context.Reference holding a Predicate<HttpServerRequest>. When it returns
true for a request, tracer produces no span for that request. Defaults to
constFalse (never disabled).
import { Layer } from "effect"import { HttpMiddleware } from "effect/unstable/http"
const NoTraceForMetrics = Layer.succeed(HttpMiddleware.TracerDisabledWhen)( (request) => request.url.startsWith("/metrics"))// => /metrics requests are not tracedlayerTracerDisabledForUrls
Section titled “layerTracerDisabledForUrls”Convenience constructor that builds a TracerDisabledWhen layer disabling
tracing for requests whose URL exactly matches one of the supplied URLs.
import { HttpMiddleware } from "effect/unstable/http"
const NoTraceLayer = HttpMiddleware.layerTracerDisabledForUrls([ "/healthz", "/metrics"])// => exact-match URLs above are excluded from tracingsearchParamsParser
Section titled “searchParamsParser”Parses the current request URL’s search parameters and provides them as
HttpServerRequest.ParsedSearchParams, removing that requirement from the
wrapped handler. (Routes served through HttpRouter already receive
ParsedSearchParams; this middleware is for handlers built outside the router.)
import { Effect } from "effect"import { HttpMiddleware, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
const handler = HttpMiddleware.searchParamsParser( Effect.gen(function*() { const params = yield* HttpServerRequest.ParsedSearchParams // GET /search?q=effect&tag=a&tag=b // => params = { q: "effect", tag: ["a", "b"] } return HttpServerResponse.text(JSON.stringify(params)) }))xForwardedHeaders
Section titled “xForwardedHeaders”Trusts reverse-proxy headers: when X-Forwarded-Host is present it rewrites the
request host header, and it sets the request’s remoteAddress from the first
entry of X-Forwarded-For. Apply this at the edge of your server when running
behind a trusted proxy/load balancer.
import { HttpMiddleware, HttpRouter } from "effect/unstable/http"
const ServerLayer = HttpRouter.serve(App, { middleware: HttpMiddleware.xForwardedHeaders})// request headers: x-forwarded-host: api.example.com, x-forwarded-for: 203.0.113.5, 10.0.0.1// => request.headers.host becomes "api.example.com"// => request.remoteAddress becomes Option.some("203.0.113.5")HttpMiddleware (type) and HttpMiddleware.Applied
Section titled “HttpMiddleware (type) and HttpMiddleware.Applied”HttpMiddleware is the function interface shown in Mental model.
The HttpMiddleware.Applied<A, E, R> namespace type represents middleware
already specialized to a concrete transformed app type A — useful when typing
a middleware whose output effect type you want to pin down.
import type { HttpMiddleware } from "effect/unstable/http"
// A middleware whose result is exactly the app type A:declare const mw: HttpMiddleware.HttpMiddlewareRouter middleware reference
Section titled “Router middleware reference”These live in effect/unstable/http/HttpRouter and build on the middleware
above.
HttpRouter.middleware
Section titled “HttpRouter.middleware”Creates route-scoped (or, with { global: true }, global) router middleware.
Provide its .layer to the route layers it should wrap. The optional type
config records what the middleware provides and which errors it handles, so
the router can subtract those from each route’s requirements at the type level.
import { Effect, Layer } from "effect"import { HttpMiddleware, HttpRouter, HttpServerResponse } from "effect/unstable/http"
// Wrap a built-in HttpMiddleware as router middleware:const CorsMiddleware = HttpRouter.middleware(HttpMiddleware.cors()).layer
const Route = HttpRouter.add( "GET", "/", Effect.succeed(HttpServerResponse.text("hi"))).pipe(Layer.provide(CorsMiddleware))To install for every route, pass { global: true }, which returns a layer
directly instead of a Middleware value:
import { HttpMiddleware, HttpRouter } from "effect/unstable/http"
const GlobalCors = HttpRouter.middleware(HttpMiddleware.cors(), { global: true})HttpRouter.cors
Section titled “HttpRouter.cors”Shortcut for global CORS: HttpRouter.middleware(HttpMiddleware.cors(options), { global: true }).
Takes the same options as HttpMiddleware.cors, except allowedOrigins here is
restricted to ReadonlyArray<string> (no predicate form).
import { HttpRouter } from "effect/unstable/http"
const CorsLayer = HttpRouter.cors({ allowedOrigins: ["https://app.example.com"] })// => Layer<never, never, HttpRouter> applied to all routesHttpRouter.disableLogger
Section titled “HttpRouter.disableLogger”A prebuilt layer (HttpRouter.middleware(HttpMiddleware.withLoggerDisabled).layer)
that suppresses the access logger for the routes it is provided to.
import { Effect, Layer } from "effect"import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
const Route = HttpRouter.add( "GET", "/ping", Effect.succeed(HttpServerResponse.text("pong"))).pipe(Layer.provide(HttpRouter.disableLogger))HttpRouter.provideRequest
Section titled “HttpRouter.provideRequest”Provides request-level dependencies to some routes by building a Layer and
supplying its services to the wrapped handlers (a thin wrapper over
HttpRouter.middleware<{ provides }>). Use it when a middleware-provided service
comes from its own layer.
import { Context, Effect, Layer } from "effect"import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
class Db extends Context.Service<Db, { readonly url: string }>()("Db") {}const DbLayer = Layer.succeed(Db)({ url: "postgres://..." })
const Route = HttpRouter.add( "GET", "/users", Effect.gen(function*() { const db = yield* Db // provided per-request return HttpServerResponse.text(db.url) })).pipe(HttpRouter.provideRequest(DbLayer))See also
Section titled “See also”- HTTP Server for routing, request decoding, and serving.
- The HTTP API builder layers its own typed middleware
(
HttpApiMiddleware) on top of this module.