Skip to content

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 can yield* CurrentUser without ever providing it itself.
  • requires — services the middleware implementation itself needs.
  • error — a Schema for 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 matching RpcMiddleware.layerClient wrapper.

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.

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

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

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

Everything below is exported from effect/unstable/rpc/RpcMiddleware.

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

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

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 unchanged

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 failing

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 directly

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 }

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 set

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 types

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 key

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>> + metadata

Type-level extractor for the services a middleware provides.

import type { RpcMiddleware } from "effect/unstable/rpc"
type _P = RpcMiddleware.Provides<typeof Auth>
// => CurrentUser

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)

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 it

Extracts the Schema declared as a middleware’s error.

import type { RpcMiddleware } from "effect/unstable/rpc"
type _E = RpcMiddleware.ErrorSchema<typeof Auth>
// => the Unauthorized schema

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>
// => Unauthorized

RpcMiddleware.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 fields

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"
  • Defining RPCs — the Rpc.make / RpcGroup.make building blocks middleware attaches to.
  • Client and server — implementing handlers, serving, and the typed client that client middleware wraps.