Specta LogoSpecta
1.0.0-rc.x (alpha)

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

Edit on GitHub

Last updated on

On this page