Overview
Wrap resolvers with shared request and response behavior.
Middleware runs around a procedure's resolver. It can inspect input, reject a call, change context, or transform a result. In the experimental API, attach it to a procedure builder with .with(...).
Log timing
use rspc::{middleware::Middleware, Procedure};
let procedure = Procedure::builder::<ApiError>()
.with(Middleware::new(|ctx: (), input: (), next| async move {
let start = std::time::Instant::now();
let result = next.exec(ctx, input).await;
println!("request took {:?}", start.elapsed());
result
}))
.query(|_: (), _: ()| async { Ok("hello") });ApiError is the error type from the procedure guide. next.exec(ctx, input) calls the rest of the chain. Return an error without calling next to reject the request.
Change context
Middleware can require authentication before the resolver runs:
struct PublicCtx {
user_id: Option<String>,
}
struct AuthCtx {
user_id: String,
}
let procedure = Procedure::builder::<ApiError>()
.with(Middleware::new(|ctx: PublicCtx, input: (), next| async move {
let Some(user_id) = ctx.user_id else {
return Err(ApiError::Unauthorized);
};
next.exec(AuthCtx { user_id }, input).await
}))
.query(|ctx: AuthCtx, _: ()| async move { Ok(ctx.user_id) });Add Unauthorized to your application error enum. The transport provides PublicCtx; the resolver only sees AuthCtx after the middleware accepts the call. The example assumes the user ID came from a verified session, not an untrusted header.
Reuse middleware
Put shared middleware in a base procedure builder. This makes the required policy explicit when each procedure is defined. A public procedure does not accidentally inherit authentication from its position in a router.
Available extensions
- Tracing records resolver timing and debug values.
- Cache, Zer, invalidation, and OpenAPI contain incomplete prototypes. Read their limitations before using them.
Last updated on
