Middleware & Auth
HttpApi middleware is a typed, schema-aware layer that runs around endpoints.
Unlike raw HTTP middleware it is part of the API definition, so it can:
- declare security schemes (bearer, API key, basic) that are decoded for you and documented in OpenAPI,
- provide services (such as the current user) to downstream middleware and handlers, and
- declare the errors it may raise, which then appear in the endpoint’s error channel and in the generated client.
Middleware is split into a definition (shared with clients) and an implementation (server-only), exactly like the API itself.
Defining the middleware
Section titled “Defining the middleware”HttpApiMiddleware.Service declares the middleware. The type parameter object
describes what it provides and requires; the options describe its security
schemes and error type.
import { Context, Schema } from "effect"import { HttpApiMiddleware, HttpApiSecurity } from "effect/unstable/httpapi"import type { User } from "../domain/User.ts"
// The service this middleware injects for downstream handlers to consumeexport class CurrentUser extends Context.Service<CurrentUser, User>()( "acme/HttpApi/Authorization/CurrentUser") {}
// The error this middleware can raise, with its HTTP statusexport class Unauthorized extends Schema.TaggedErrorClass<Unauthorized>()( "Unauthorized", { message: Schema.String }, { httpApiStatus: 401 }) {}
export class Authorization extends HttpApiMiddleware.Service<Authorization, { // Services made available to endpoints/middleware that run after this one provides: CurrentUser // Services this middleware needs from earlier middleware (none here) requires: never}>()("acme/HttpApi/Authorization", { // Clients must also implement this middleware (e.g. to attach a token) requiredForClient: true, // Security schemes power OpenAPI docs *and* decode credentials for you security: { bearer: HttpApiSecurity.bearer }, // Errors this middleware may fail with error: Unauthorized}) {}The Service type-parameter object
Section titled “The Service type-parameter object”The first call — HttpApiMiddleware.Service<Self, Config>() — accepts an
optional config object with two keys:
| Key | Meaning |
|---|---|
provides | Services this middleware makes available to everything that runs after it (other middleware and the endpoint handler). Removed from the handler’s requirement type. |
requires | Services this middleware needs in order to run (typically provided by an earlier middleware in the chain). Added to the overall requirement type. |
Both default to never. Internally these drive HttpApiMiddleware.ApplyServices<A, R>,
which removes Provides<A> from a handler’s requirements and adds Requires<A>.
The Service options object
Section titled “The Service options object”The second call — (id, options?) — takes the middleware id string and an
options object:
| Option | Type | Meaning |
|---|---|---|
error | Schema.Top | ReadonlyArray<Schema.Top> | Error schema(s) this middleware may fail with. They are merged into the endpoint error surface and the generated client, so they must be Schema values. Defaults to no errors. |
security | Record<string, HttpApiSecurity> | Map of scheme name → HttpApiSecurity. Declaring this makes the middleware a security middleware: it receives decoded credentials and the server implementation provides one handler per key. Must be non-empty (an empty object throws at definition time). |
requiredForClient | boolean | When true, the generated client must also install a matching client middleware (via layerClient). Defaults to false. |
import { Schema } from "effect"import { HttpApiMiddleware, HttpApiSecurity } from "effect/unstable/httpapi"
class RateLimited extends Schema.TaggedErrorClass<RateLimited>()( "RateLimited", { retryAfter: Schema.Number }, { httpApiStatus: 429 }) {}
// A plain (non-security) middleware: no `security`, so no credentials are decodedexport class RateLimit extends HttpApiMiddleware.Service<RateLimit>()( "acme/HttpApi/RateLimit", { error: RateLimited }) {}
// A security middleware reading an API key from a headerexport class ApiKeyAuth extends HttpApiMiddleware.Service<ApiKeyAuth>()( "acme/HttpApi/ApiKeyAuth", { security: { apiKey: HttpApiSecurity.apiKey({ key: "x-api-key" }) } }) {}Applying middleware
Section titled “Applying middleware”Attach middleware to a whole group with .middleware(...), or to a single
endpoint. Group-level is the common case for auth:
// api/Users.ts (excerpt)import { HttpApiGroup } from "effect/unstable/httpapi"import { Authorization } from "./Authorization.ts"
class UsersApiGroup extends HttpApiGroup.make("users") .add(/* ...endpoints... */) // Every endpoint in this group now requires authorization, and `CurrentUser` // becomes available to each handler. .middleware(Authorization) .prefix("/users"){}To guard a single endpoint instead, call .middleware(...) on the endpoint
before adding it to the group:
// Only this endpoint requires Authorization; siblings stay publicHttpApiEndpoint.get("me", "/me") .addSuccess(User) .middleware(Authorization)Implementing the middleware (server)
Section titled “Implementing the middleware (server)”The implementation is a Layer providing the Authorization service. Because
the definition declared a bearer security scheme, the implementation receives
the already-decoded credential. Validate it and use Effect.provideService to
inject the resolved CurrentUser into the continuation (httpEffect).
import { Effect, Layer, Redacted } from "effect"import { Authorization, CurrentUser, Unauthorized } from "../api/Authorization.ts"import { User, UserId } from "../domain/User.ts"
export const AuthorizationLayer = Layer.effect( Authorization, Effect.gen(function*() { // Resolve middleware dependencies once (a DB, an auth provider, ...) yield* Effect.logInfo("Starting Authorization middleware")
return Authorization.of({ // One handler per declared security scheme. `credential` is decoded from // the Authorization header and wrapped in Redacted so it never logs. bearer: Effect.fn(function*(httpEffect, { credential }) { const token = Redacted.value(credential) if (token !== "dev-token") { // Failing here short-circuits the request with a 401 return yield* new Unauthorized({ message: "Missing or invalid bearer token" }) }
// Provide CurrentUser to everything downstream: other middleware and // the endpoint handler. return yield* Effect.provideService( httpEffect, CurrentUser, new User({ id: UserId.make(1), name: "Dev User", email: "dev@acme.com" }) ) }) }) }))The handler from the previous page can now read the injected user:
// inside HttpApiBuilder.group(Api, "users", ...).handle("me", () => CurrentUser) // returns the injected current userA plain (non-security) middleware has a single function instead of a
per-scheme record — it receives only httpEffect and the endpoint/group
metadata, with no credential:
import { Effect, Layer } from "effect"import { RateLimit } from "../api/RateLimit.ts"
export const RateLimitLayer = Layer.succeed( RateLimit, // (httpEffect, { endpoint, group }) => Effect.Effect<HttpServerResponse, ...> (httpEffect, { endpoint }) => Effect.tap(httpEffect, () => Effect.logDebug(`served ${endpoint.name}`)))HttpApiSecurity reference
Section titled “HttpApiSecurity reference”HttpApiSecurity describes where a credential is read from and which
credential type is handed to your security middleware. The schemes do not
authenticate by themselves — they decode the request and feed your middleware
handler, and they drive OpenAPI securitySchemes generation.
The union is HttpApiSecurity = Http | ApiKey | Basic, and every scheme carries a
decoded credential type recoverable via HttpApiSecurity.Type<S>.
A generic HTTP Authorization scheme. The credential is decoded by stripping the
scheme prefix from the Authorization header and is delivered as a
Redacted<string>. bearer is just http({ scheme: "Bearer" }).
import { HttpApiSecurity } from "effect/unstable/httpapi"
const githubToken = HttpApiSecurity.http({ scheme: "token" })// reads `Authorization: token <value>` -> credential: Redacted<string>// => { _tag: "Http", scheme: "token", ... }bearer
Section titled “bearer”The standard Authorization: Bearer <token> scheme. Pre-built convenience value
equal to http({ scheme: "Bearer" }); the credential is a Redacted<string>.
import { HttpApiSecurity } from "effect/unstable/httpapi"
HttpApiSecurity.bearer// reads `Authorization: Bearer <token>` -> credential: Redacted<string>// => { _tag: "Http", scheme: "Bearer", ... }apiKey
Section titled “apiKey”An API-key scheme. key is the parameter name and in selects the source —
"header" (default), "query", or "cookie". The credential is a
Redacted<string>. For "header", the key is matched case-insensitively (HTTP
header normalization); for "query" and "cookie" it is matched exactly.
import { HttpApiSecurity } from "effect/unstable/httpapi"
HttpApiSecurity.apiKey({ key: "x-api-key" })// in defaults to "header" -> { _tag: "ApiKey", key: "x-api-key", in: "header", ... }
HttpApiSecurity.apiKey({ key: "api_key", in: "query" })// reads ?api_key=... -> { _tag: "ApiKey", key: "api_key", in: "query", ... }
HttpApiSecurity.apiKey({ key: "session", in: "cookie" })// reads the `session` cookie -> { _tag: "ApiKey", key: "session", in: "cookie", ... }HTTP Basic authentication. Reads Authorization: Basic <base64> and decodes it
into a Credentials object: { username: string; password: Redacted<string> }.
The username is exposed in plain text; the password is redacted.
import { HttpApiSecurity } from "effect/unstable/httpapi"
HttpApiSecurity.basic// reads `Authorization: Basic <base64(user:pass)>`// -> credential: { username: string, password: Redacted<string> }// => { _tag: "Basic", ... }annotate
Section titled “annotate”Adds a single OpenAPI annotation value to a scheme, keyed by a Context.Key.
Used to enrich generated docs; it does not change runtime decoding. Dual: pipeable
or data-first.
import { HttpApiSecurity, OpenApi } from "effect/unstable/httpapi"
const documented = HttpApiSecurity.bearer.pipe( HttpApiSecurity.annotate(OpenApi.Description, "JWT access token"))// same runtime scheme, now carrying an OpenAPI description annotationannotateMerge
Section titled “annotateMerge”Merges an entire Context.Context of annotations into a scheme at once (instead
of one key/value at a time). Also dual.
import { Context } from "effect"import { HttpApiSecurity } from "effect/unstable/httpapi"
declare const annotations: Context.Context<never>
const documented = HttpApiSecurity.annotateMerge(HttpApiSecurity.bearer, annotations)// bearer scheme with the supplied annotation context merged inHttpApiSecurity.Type
Section titled “HttpApiSecurity.Type”Type-level helper that extracts the decoded credential type of a scheme. Useful when typing middleware handlers by hand.
import type { HttpApiSecurity } from "effect/unstable/httpapi"
type Bearer = HttpApiSecurity.HttpApiSecurity.Type<HttpApiSecurity.Http>// => Redacted<string>type Basic = HttpApiSecurity.HttpApiSecurity.Type<HttpApiSecurity.Basic>// => { readonly username: string; readonly password: Redacted<string> }HttpApiMiddleware reference
Section titled “HttpApiMiddleware reference”Service
Section titled “Service”The constructor for middleware service keys — documented in full above. Returns a
Context.Service class augmented with the middleware metadata (provided/required
services, error schemas, security schemes, client requirements) used by groups,
the builder, and generated clients.
import { HttpApiMiddleware, HttpApiSecurity } from "effect/unstable/httpapi"
class Auth extends HttpApiMiddleware.Service<Auth>()("acme/Auth", { security: { bearer: HttpApiSecurity.bearer }}) {}// => a Context.Service class; HttpApiMiddleware.isSecurity(Auth) === trueisSecurity
Section titled “isSecurity”Type guard that returns true when a middleware service was declared with a
security option (and therefore decodes credentials).
import { HttpApiMiddleware, HttpApiSecurity } from "effect/unstable/httpapi"
class Auth extends HttpApiMiddleware.Service<Auth>()("acme/Auth", { security: { bearer: HttpApiSecurity.bearer }}) {}class Log extends HttpApiMiddleware.Service<Log>()("acme/Log") {}
HttpApiMiddleware.isSecurity(Auth) // => trueHttpApiMiddleware.isSecurity(Log) // => falselayerSchemaErrorTransform
Section titled “layerSchemaErrorTransform”Builds a middleware Layer that catches HttpApiSchemaError failures (raised
while decoding request payloads / encoding responses) and maps them to one of the
middleware’s declared error schemas. The transform must fail with an error
covered by the middleware’s error option.
import { Effect, Schema } from "effect"import { HttpApiMiddleware } from "effect/unstable/httpapi"
class CustomError extends Schema.TaggedErrorClass<CustomError>()("CustomError", {}) {}
class ErrorHandler extends HttpApiMiddleware.Service<ErrorHandler>()( "api/ErrorHandler", { error: CustomError }) {}
export const ErrorHandlerLayer = HttpApiMiddleware.layerSchemaErrorTransform( ErrorHandler, (schemaError) => Effect.log("Got SchemaError", schemaError).pipe( Effect.andThen(Effect.fail(new CustomError())) ))// => Layer.Layer<ErrorHandler>layerClient
Section titled “layerClient”Provides the client-side implementation for a middleware declared with
requiredForClient: true. The function receives { endpoint, group, request, next } and returns the response from next(...); the layer captures its
surrounding services and publishes them through the ForClient marker that the
generated client consumes.
import { Effect } from "effect"import { HttpClientRequest } from "effect/unstable/http"import { HttpApiMiddleware } from "effect/unstable/httpapi"import { Authorization } from "../api/Authorization.ts"
export const AuthorizationClient = HttpApiMiddleware.layerClient( Authorization, Effect.fn(function*({ next, request }) { // Modify the request, then pass it down the chain. Here we attach a token. return yield* next(HttpClientRequest.bearerToken(request, "dev-token")) }))// => Layer.Layer<ForClient<Authorization>>This client layer is provided when constructing the client — see Serving & clients for the full wiring.
Service models (type-level)
Section titled “Service models (type-level)”These types are exported for advanced typing and are rarely written by hand, but they describe the shapes the builder relies on:
| Type | Description |
|---|---|
HttpApiMiddleware<Provides, E, Requires> | A plain server middleware function: (httpEffect, { endpoint, group }) => Effect<HttpServerResponse, ...>. |
HttpApiMiddlewareSecurity<Security, Provides, E, Requires> | A security middleware: a record keyed by scheme name, each value receiving { credential, endpoint, group }. |
HttpApiMiddlewareClient<E, CE, R> | A client middleware function receiving { endpoint, group, request, next }. |
Provides<A> / Requires<A> | Extract the provided / required services from a middleware id. |
ApplyServices<A, R> | Exclude<R, Provides<A>> | Requires<A> — how middleware rewrites a handler’s requirements. |
Error<A> / ErrorSchema<A> | Extract the decoded error type / error schema of a middleware. |
ClientError<A> / MiddlewareClient<A> | Client error type / ForClient marker for requiredForClient: true middleware. |
ForClient<Id> | The service marker installed by layerClient. |
HttpApiBuilder security helpers
Section titled “HttpApiBuilder security helpers”These low-level helpers live in HttpApiBuilder and are useful when you decode or
set credentials manually (for example, in cookie-based login/logout flows). When
you attach a security middleware to a group, the builder already calls
securityDecode for you and passes the result as credential — reach for these
only for bespoke flows.
securityDecode
Section titled “securityDecode”Decodes the credential for a given HttpApiSecurity scheme from the current
request. Returns the scheme’s credential type and requires HttpServerRequest
(plus parsed search params for query keys). On a missing/invalid credential it
yields an empty Redacted("") (or empty Credentials for basic) rather than
failing.
import { Effect, Redacted } from "effect"import { HttpApiBuilder, HttpApiSecurity } from "effect/unstable/httpapi"
const decodeKey = Effect.gen(function*() { const credential = yield* HttpApiBuilder.securityDecode( HttpApiSecurity.apiKey({ key: "session", in: "cookie" }) ) return Redacted.value(credential) // the raw cookie value, or "" if absent})// => Effect<string, never, HttpServerRequest | ParsedSearchParams>securitySetCookie
Section titled “securitySetCookie”Registers a pre-response handler that writes an API-key cookie onto the outgoing
response — the server side of cookie-based auth. The cookie defaults to secure
and httpOnly; pass options to override (e.g. sameSite, path, maxAge).
Accepts the cookie value as a string or Redacted<string>.
import { Effect } from "effect"import { HttpApiBuilder, HttpApiSecurity } from "effect/unstable/httpapi"
const sessionKey = HttpApiSecurity.apiKey({ key: "session", in: "cookie" })
const login = Effect.gen(function*() { // ...authenticate the user, mint a session token... yield* HttpApiBuilder.securitySetCookie(sessionKey, "session-token-123", { sameSite: "strict", path: "/" }) // response now carries `Set-Cookie: session=session-token-123; Secure; HttpOnly; ...`})// => Effect<void, never, HttpServerRequest>