Skip to content

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 router
const CorsLayer = HttpRouter.cors()
const App = Layer.mergeAll(HelloRoute, CorsLayer)

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 middleware option of HttpRouter.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 access logger is installed by default.

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))
  • HttpRouter.cors(options?) — installs HttpMiddleware.cors globally for the router. Returns a Layer<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)
)

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.

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
)

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 request

Handles 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 appended

The options object (all fields optional):

FieldTypeDefaultEffect
allowedOriginsReadonlyArray<string> | Predicate<string>[][] emits access-control-allow-origin: *; one origin pins it (with Vary: Origin); many or a predicate reflects the matching Origin.
allowedMethodsReadonlyArray<string>["GET","HEAD","PUT","PATCH","POST","DELETE"]access-control-allow-methods on preflight.
allowedHeadersReadonlyArray<string>[]access-control-allow-headers; empty reflects the request’s Access-Control-Request-Headers.
exposedHeadersReadonlyArray<string>[]access-control-expose-headers.
credentialsbooleanfalsewhen true, adds access-control-allow-credentials: true.
maxAgenumberundefinedaccess-control-max-age (seconds).

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.* attributes

Tracing is skipped when the fiber’s TracerEnabled ref is off, or when TracerDisabledWhen matches the request.

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"

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 traced

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 tracing

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))
})
)

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.HttpMiddleware

These live in effect/unstable/http/HttpRouter and build on the middleware above.

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
})

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 routes

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))

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))
  • HTTP Server for routing, request decoding, and serving.
  • The HTTP API builder layers its own typed middleware (HttpApiMiddleware) on top of this module.