Skip to content

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.

HttpApiMiddleware.Service declares the middleware. The type parameter object describes what it provides and requires; the options describe its security schemes and error type.

api/Authorization.ts
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 consume
export class CurrentUser extends Context.Service<CurrentUser, User>()(
"acme/HttpApi/Authorization/CurrentUser"
) {}
// The error this middleware can raise, with its HTTP status
export 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 first call — HttpApiMiddleware.Service<Self, Config>() — accepts an optional config object with two keys:

KeyMeaning
providesServices this middleware makes available to everything that runs after it (other middleware and the endpoint handler). Removed from the handler’s requirement type.
requiresServices 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 second call — (id, options?) — takes the middleware id string and an options object:

OptionTypeMeaning
errorSchema.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.
securityRecord<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).
requiredForClientbooleanWhen 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 decoded
export class RateLimit extends HttpApiMiddleware.Service<RateLimit>()(
"acme/HttpApi/RateLimit",
{ error: RateLimited }
) {}
// A security middleware reading an API key from a header
export class ApiKeyAuth extends HttpApiMiddleware.Service<ApiKeyAuth>()(
"acme/HttpApi/ApiKeyAuth",
{ security: { apiKey: HttpApiSecurity.apiKey({ key: "x-api-key" }) } }
) {}

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 public
HttpApiEndpoint.get("me", "/me")
.addSuccess(User)
.middleware(Authorization)

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

server/Authorization.ts
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 user

A 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 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", ... }

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", ... }

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", ... }

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 annotation

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 in

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

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

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) // => true
HttpApiMiddleware.isSecurity(Log) // => false

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>

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.

These types are exported for advanced typing and are rarely written by hand, but they describe the shapes the builder relies on:

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

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.

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>

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>