Parameterized Middleware
In addition to inline closure-based middleware registered via wrap() and with(), Volga also supports registering middleware as reusable, configurable types through the attach() method.
This approach is ideal when your middleware needs its own state, configuration, or is meant to be reused across multiple applications.
Overview
A parameterized middleware is a regular Rust type (typically a struct) that implements the Middleware trait. The type holds any configuration or shared state the middleware needs, and its call() method contains the middleware logic.
This is very similar to middleware patterns found in other ecosystems (for example, Tower layers, ASP.NET Core middleware, or Express.js classes).
The Middleware Trait
The trait is defined as follows:
pub trait Middleware: Send + Sync + 'static {
fn call(
&self,
ctx: HttpContext,
next: NextFn,
) -> impl Future<Output = HttpResult> + Send + 'static;
}
Any type implementing this trait can be passed to attach().
Example: A Timeout Middleware
Here is a small middleware that adds an artificial delay before the request is processed further. Its duration is configurable at registration time:
use std::time::Duration;
use volga::{App, HttpResult, middleware::{HttpContext, NextFn, Middleware}};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new();
// Register the parameterized middleware
app.attach(Timeout {
duration: Duration::from_secs(1),
});
app.map_get("/hello", || async { "Hello, World!" });
app.run().await
}
struct Timeout {
duration: Duration,
}
impl Middleware for Timeout {
fn call(&self, ctx: HttpContext, next: NextFn) -> impl Future<Output = HttpResult> + 'static {
let duration = self.duration;
async move {
tokio::time::sleep(duration).await;
next(ctx).await
}
}
}
The Timeout struct carries its configuration (duration) and implements the middleware logic inside call(). You can instantiate it multiple times with different durations, or share the same instance across routes.
.wrap() vs .attach()
Both methods register middleware operating on the full HttpContext, but they target different use cases:
wrap()is optimized for short, inline closures. No type annotations onctxandnextare required.attach()is intended for reusable, parameterized middleware types — typicallystructs that implement theMiddlewaretrait.
Tips
Use wrap() for quick inline middleware and attach() when you want to package middleware as a named, configurable type you can reuse.
attach() also accepts closures, but type annotations on the arguments are required:
use volga::{App, middleware::{HttpContext, NextFn}};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new();
app.attach(|ctx: HttpContext, next: NextFn| async move {
next(ctx).await
});
app.run().await
}
Registering on Routes and Route Groups
Parameterized middleware can also be attached to individual routes and route groups, not just to the entire application:
use std::time::Duration;
use volga::{App, HttpResult, middleware::{HttpContext, NextFn, Middleware}};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new();
// Attach to a single route
app
.map_get("/hello", || async { "Hello, World!" })
.attach(Timeout { duration: Duration::from_secs(1) });
// Attach to a route group
app.group("/api", |api| {
api.attach(Timeout { duration: Duration::from_secs(2) });
api.map_get("/ping", || async { "pong" });
});
app.run().await
}
struct Timeout {
duration: Duration,
}
impl Middleware for Timeout {
fn call(&self, ctx: HttpContext, next: NextFn) -> impl Future<Output = HttpResult> + 'static {
let duration = self.duration;
async move {
tokio::time::sleep(duration).await;
next(ctx).await
}
}
}
When to Prefer Parameterized Middleware
Reach for attach() and a dedicated type when:
- The middleware needs configuration at registration time (timeouts, limits, feature flags, etc.).
- The middleware should be reusable across projects or crates.
- The middleware holds shared state, counters, or handles to external systems.
- You want to unit test the middleware independently of a running application.
For simple, one-off transformations, keeping the logic inline with wrap() or with() is typically more concise.
Built-in features such as CORS, authentication and rate limiting are themselves implemented as parameterized middleware on top of attach().
Other Middleware Variants
The same parameterized approach works for the other middleware traits as well. Besides Middleware, you can implement Filter, TapReq, MapOk, MapErr, and With on your own types. They are registered using the same methods as their closure counterparts — filter(), tap_req(), map_ok(), map_err(), and with() respectively. For example, a reusable parameterized filter:
use volga::{App, headers::HttpHeaders, middleware::Filter};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new();
app.map_get("/sum/{x}/{y}", |x: i32, y: i32| async move { x + y })
.filter(HasHeader {
header: "x-api-key".to_owned()
});
app.run().await
}
#[derive(Clone)]
struct HasHeader {
header: String
}
impl Filter<HttpHeaders> for HasHeader {
type Output = bool;
async fn filter(&self, headers: HttpHeaders) -> bool {
headers.get_raw(&self.header).is_some()
}
}