Middleware
RPC middleware is a Context.Service that wraps
handler execution on the server and can optionally install a client-side
wrapper for generated clients. A single middleware definition records four
things at the type level:
provides— services the middleware injects into downstream handlers, so the handler canyield* CurrentUserwithout ever providing it itself.requires— services the middleware implementation itself needs.error— aSchemafor failures the middleware can raise across the RPC boundary (e.g.Unauthorized). This is unioned into every guarded RPC’s error channel.clientError/requiredForClient— client-only metadata for the matchingRpcMiddleware.layerClientwrapper.
The mental model: a server middleware receives the handler effect plus the
request metadata (client, requestId, rpc, payload, headers) and
returns a new effect. provides removes a service from the handler’s
requirements; requires adds the middleware’s own dependencies.
Defining a middleware
Section titled “Defining a middleware”Here is a realistic authentication middleware. It declares an Unauthorized
error, provides a CurrentUser service to handlers, and reads the request
headers to authenticate.
import { Context, Effect, Schema } from "effect"import { RpcMiddleware } from "effect/unstable/rpc"
// The service the middleware will inject into every guarded handler.class CurrentUser extends Context.Service<CurrentUser, { readonly id: string readonly name: string}>()("app/CurrentUser") {}
// A failure the middleware can raise. Because it crosses the RPC boundary it// MUST be a Schema (here a tagged error). It is added to the error channel of// every RPC the middleware guards.class Unauthorized extends Schema.TaggedErrorClass<Unauthorized>()( "Unauthorized", { reason: Schema.String }) {}
// `RpcMiddleware.Service<Self, Config>()(id, options?)`:// - the Config type param carries `provides` / `requires` / `clientError`// - the runtime `options` carries the `error` schema and `requiredForClient`class AuthMiddleware extends RpcMiddleware.Service<AuthMiddleware, { provides: CurrentUser}>()("app/AuthMiddleware", { error: Unauthorized, // Force typed clients to install a matching client middleware layer. requiredForClient: true}) {}The server implementation is the value of the service: a function matching
RpcMiddleware<Provides, E, Requires>. It receives the handler effect and the
request options, and returns the wrapped effect. Provide it like any other
Context.Service with Layer.succeed (or Layer.effect when the
implementation needs setup).
import { Effect, Layer } from "effect"
// `AuthMiddleware.of` types the function for you. The first argument is the// handler effect; the second is the request metadata.export const AuthLive = Layer.succeed(AuthMiddleware)( AuthMiddleware.of((effect, { headers }) => Effect.gen(function*() { const userId = headers["x-user-id"] if (userId === undefined) { // The declared `error` schema — surfaces to the caller, typed. return yield* Effect.fail( new Unauthorized({ reason: "missing x-user-id header" }) ) } // `provides: CurrentUser` means we satisfy that requirement here. The // handler can `yield* CurrentUser` and never has to provide it. return yield* Effect.provideService(effect, CurrentUser, { id: userId, name: headers["x-user-name"] ?? "anonymous" }) }) ))A middleware that does cross-cutting work without injecting anything is even
smaller. It takes no provides/requires and (here) declares no error:
import { Effect, Layer, Metric } from "effect"import { RpcMiddleware } from "effect/unstable/rpc"
class TimingMiddleware extends RpcMiddleware.Service<TimingMiddleware>()( "app/TimingMiddleware") {}
const successes = Metric.counter("rpc_success")const defects = Metric.counter("rpc_defect")
export const TimingLive = Layer.succeed(TimingMiddleware)( TimingMiddleware.of((effect) => effect.pipe( Effect.tap(Metric.update(successes, 1)), Effect.tapDefect(() => Metric.update(defects, 1)) ) ))Attaching middleware
Section titled “Attaching middleware”Attach a middleware to a single procedure with rpc.middleware(M), or to a
whole group with group.middleware(M). Group-level middleware applies to
every RPC added to the group so far — so order matters: add the RPCs that
need it, then call .middleware.
import { Schema } from "effect"import { Rpc, RpcGroup } from "effect/unstable/rpc"
const User = Schema.Struct({ id: Schema.String, name: Schema.String })
export const UserRpcs = RpcGroup.make( // Per-RPC: only this procedure is timed. Rpc.make("Ping", { success: Schema.String }).middleware(TimingMiddleware),
Rpc.make("GetUser", { payload: { id: Schema.String }, success: User }), Rpc.make("DeleteUser", { payload: { id: Schema.String } }) // Group-level: every RPC above is now guarded by AuthMiddleware. The // `Unauthorized` error and the provided `CurrentUser` propagate to all of // them.).middleware(AuthMiddleware)Because AuthMiddleware declares provides: CurrentUser, the handler can read
CurrentUser from the environment and that requirement is removed from the
handler layer’s requirements (Rpc.ExcludeProvides does this in the types). You
never have to provide CurrentUser to toLayer:
import { Effect } from "effect"
export const UserRpcsLayer = UserRpcs.toLayer( Effect.gen(function*() { return { Ping: () => Effect.succeed("pong"), // `CurrentUser` is injected by AuthMiddleware — just pull it. GetUser: ({ id }) => Effect.gen(function*() { const me = yield* CurrentUser return { id, name: me.name } }), DeleteUser: ({ id }) => Effect.log(`deleting ${id}`) } }))Finally provide the middleware implementation alongside the handlers when you
build the server. RpcServer.layer reads the group’s middleware set, so the
AuthMiddleware and TimingMiddleware service layers must be in scope:
import { Layer } from "effect"import { RpcServer } from "effect/unstable/rpc"
export const RpcLive = RpcServer.layer(UserRpcs).pipe( Layer.provide([UserRpcsLayer, AuthLive, TimingLive]))Client middleware
Section titled “Client middleware”RpcMiddleware.layerClient(tag, service) installs a client-side wrapper
(RpcMiddlewareClient) that runs for outgoing requests. It receives
{ rpc, request, next } and can inspect, rewrite, retry, or short-circuit the
request before calling next. The most common use is injecting an auth header
or token.
import { RpcMiddleware } from "effect/unstable/rpc"import { Headers } from "effect/unstable/http"
// Match the SAME tag as the server middleware. The client wrapper rewrites the// outgoing request's headers, then forwards it with `next`.export const AuthClient = RpcMiddleware.layerClient( AuthMiddleware, ({ next, request }) => next({ ...request, headers: Headers.set(request.headers, "x-user-id", "u_123") }))The service argument can also be an Effect, which is run once in the layer’s
scope — useful for reading a token service or config first:
import { Context, Effect } from "effect"import { RpcMiddleware } from "effect/unstable/rpc"import { Headers } from "effect/unstable/http"
class TokenStore extends Context.Service<TokenStore, { readonly current: Effect.Effect<string>}>()("app/TokenStore") {}
// The Effect resolves to the client middleware function. Services it requires// (here `TokenStore`) become requirements of the produced layer.export const AuthClientFromStore = RpcMiddleware.layerClient( AuthMiddleware, Effect.gen(function*() { const store = yield* TokenStore return ({ next, request }) => Effect.gen(function*() { const token = yield* store.current return yield* next({ ...request, headers: Headers.set(request.headers, "authorization", `Bearer ${token}`) }) }) }))Because AuthMiddleware was defined with requiredForClient: true, the
generated client’s type now requires the matching client layer (the
RpcMiddleware.ForClient marker). Provide AuthClient next to the client
protocol or you get a type error:
import { Layer } from "effect"import { RpcClient, RpcSerialization } from "effect/unstable/rpc"import { FetchHttpClient } from "effect/unstable/http"
// The client layer must include the ForClient marker layer because// `requiredForClient: true`.const ClientLayer = Layer.empty.pipe( Layer.provide(AuthClient), // satisfies ForClient<AuthMiddleware> Layer.provide(RpcClient.layerProtocolHttp({ url: "http://localhost:3000/rpc" })), Layer.provide(RpcSerialization.layerNdjson), Layer.provide(FetchHttpClient.layer))Exhaustive reference
Section titled “Exhaustive reference”Everything below is exported from effect/unstable/rpc/RpcMiddleware.
RpcMiddleware.Service
Section titled “RpcMiddleware.Service”Creates a typed middleware service class. The Config type parameter declares
provides / requires / clientError; the call options declare the error
schema (default Schema.Never) and requiredForClient (default false).
import { Context, Schema } from "effect"import { RpcMiddleware } from "effect/unstable/rpc"
class CurrentUser extends Context.Service<CurrentUser, { id: string }>()( "CurrentUser") {}
class Auth extends RpcMiddleware.Service<Auth, { provides: CurrentUser}>()("Auth", { error: Schema.Never }) {}// => a Context.Service class usable in `Layer.succeed(Auth)(...)`RpcMiddleware.layerClient
Section titled “RpcMiddleware.layerClient”Provides the client-side implementation for a middleware. Takes the middleware
tag and either a RpcMiddlewareClient function or an Effect that produces one;
returns a Layer<ForClient<Id>, ...>.
import { RpcMiddleware } from "effect/unstable/rpc"import { Headers } from "effect/unstable/http"
const AuthClient = RpcMiddleware.layerClient(Auth, ({ next, request }) => next({ ...request, headers: Headers.set(request.headers, "x-user-id", "1") }))// => Layer<RpcMiddleware.ForClient<Auth>>RpcMiddleware.RpcMiddleware
Section titled “RpcMiddleware.RpcMiddleware”The server middleware function type: RpcMiddleware<Provides, E, Requires>. It
takes (effect, { client, requestId, rpc, payload, headers }) and returns
Effect<SuccessValue, E | unhandled, Requires | Scope>.
import { Effect } from "effect"import type { RpcMiddleware } from "effect/unstable/rpc"
// A no-op middleware function value.const passthrough: RpcMiddleware.RpcMiddleware<never, never, never> = (effect) => effect// => returns the handler effect unchangedRpcMiddleware.RpcMiddlewareClient
Section titled “RpcMiddleware.RpcMiddlewareClient”The client middleware function type: RpcMiddlewareClient<E, CE, R>. It takes
{ rpc, request, next } and must eventually call next(request). Use it to
rewrite headers, retry, or short-circuit.
import { Effect } from "effect"import type { RpcMiddleware } from "effect/unstable/rpc"
const retryOnce: RpcMiddleware.RpcMiddlewareClient<never, never, never> = ({ next, request }) => next(request).pipe(Effect.retry({ times: 1 }))// => retries the outgoing request once before failingRpcMiddleware.SuccessValue
Section titled “RpcMiddleware.SuccessValue”The opaque marker type standing in for a handler’s concrete success value inside a middleware. You can wrap the effect but not read the value.
import type { RpcMiddleware } from "effect/unstable/rpc"
type _S = RpcMiddleware.SuccessValue// => { readonly _: unique symbol } — never inspected directlyRpcMiddleware.ForClient
Section titled “RpcMiddleware.ForClient”A marker service requirement indicating a middleware has a client-side
implementation installed. Produced by layerClient and required by clients when
requiredForClient: true.
import type { RpcMiddleware } from "effect/unstable/rpc"
type _Marker = RpcMiddleware.ForClient<typeof Auth>// => { readonly _: unique symbol; readonly id: typeof Auth }RpcMiddleware.ServiceClass
Section titled “RpcMiddleware.ServiceClass”The full class shape returned by RpcMiddleware.Service. It is a
Context.Service whose value is the server middleware function, carrying the
error schema, requiredForClient flag, and ~ClientError marker.
import type { RpcMiddleware } from "effect/unstable/rpc"
// `Auth` IS a ServiceClass instance; its static members include:Auth.error // => the Unauthorized schema (or Schema.Never)Auth.requiredForClient // => false unless setRpcMiddleware.Any
Section titled “RpcMiddleware.Any”An erased server middleware function type, accepting any provides/error/ requires — useful for storing heterogeneous middleware functions.
import type { RpcMiddleware } from "effect/unstable/rpc"
const store = new Map<string, RpcMiddleware.Any>()// => holds middleware functions regardless of their concrete typesRpcMiddleware.AnyService
Section titled “RpcMiddleware.AnyService”An erased middleware context key (Context.Key) carrying the metadata
fields (error, requiredForClient, ~ClientError). This is what
rpc.middleware(...) and group.middleware(...) accept.
import type { RpcMiddleware } from "effect/unstable/rpc"
const fn = (m: RpcMiddleware.AnyService) => m.requiredForClient// => reads the flag off any middleware service keyRpcMiddleware.AnyServiceWithProps
Section titled “RpcMiddleware.AnyServiceWithProps”Like AnyService, but its service value is known to be an
RpcMiddleware<any, any, any> function. Used internally by Rpc.AnyWithProps’s
middlewares set.
import type { RpcMiddleware } from "effect/unstable/rpc"
type _M = RpcMiddleware.AnyServiceWithProps// => Context.Key<any, RpcMiddleware<any, any, any>> + metadataRpcMiddleware.Provides
Section titled “RpcMiddleware.Provides”Type-level extractor for the services a middleware provides.
import type { RpcMiddleware } from "effect/unstable/rpc"
type _P = RpcMiddleware.Provides<typeof Auth>// => CurrentUserRpcMiddleware.Requires
Section titled “RpcMiddleware.Requires”Type-level extractor for the services a middleware requires.
import type { RpcMiddleware } from "effect/unstable/rpc"
type _R = RpcMiddleware.Requires<typeof Auth>// => never (Auth requires nothing)RpcMiddleware.ApplyServices
Section titled “RpcMiddleware.ApplyServices”Applies a middleware’s transformation to an environment type R: removes what
it provides, adds what it requires. This is Exclude<R, Provides> | Requires.
import type { RpcMiddleware } from "effect/unstable/rpc"
type _Env = RpcMiddleware.ApplyServices<typeof Auth, CurrentUser>// => never — CurrentUser is removed because Auth provides itRpcMiddleware.ErrorSchema
Section titled “RpcMiddleware.ErrorSchema”Extracts the Schema declared as a middleware’s error.
import type { RpcMiddleware } from "effect/unstable/rpc"
type _E = RpcMiddleware.ErrorSchema<typeof Auth>// => the Unauthorized schemaRpcMiddleware.Error
Section titled “RpcMiddleware.Error”Extracts the decoded error value type from a middleware’s error schema.
import type { RpcMiddleware } from "effect/unstable/rpc"
type _E = RpcMiddleware.Error<typeof Auth>// => UnauthorizedRpcMiddleware.ErrorServicesEncode / ErrorServicesDecode
Section titled “RpcMiddleware.ErrorServicesEncode / ErrorServicesDecode”Extract the schema services needed to encode (server side) or decode (client side) the middleware’s error. These services stay in the generated RPC environments — see the gotcha below.
import type { RpcMiddleware } from "effect/unstable/rpc"
type _Enc = RpcMiddleware.ErrorServicesEncode<typeof Auth>type _Dec = RpcMiddleware.ErrorServicesDecode<typeof Auth>// => never for a plain tagged error with no service-backed fieldsRpcMiddleware.TypeId
Section titled “RpcMiddleware.TypeId”The literal type id "~effect/rpc/RpcMiddleware" used to identify and inspect
middleware service classes at runtime.
import { RpcMiddleware } from "effect/unstable/rpc"
RpcMiddleware.TypeId// => "~effect/rpc/RpcMiddleware"Related
Section titled “Related”- Defining RPCs — the
Rpc.make/RpcGroup.makebuilding blocks middleware attaches to. - Client and server — implementing handlers, serving, and the typed client that client middleware wraps.