Volga
Home
API Docs
GitHub
  • English
  • Русский
Home
API Docs
GitHub
  • English
  • Русский
  • Home
  • Getting Started

    • Quick Start
    • Route Parameters
    • Query Parameters
    • Route Groups
  • Requests & Responses

    • Headers
    • Handling JSON
    • Handling Form Data
    • Working with Files
    • Cookies
  • Middleware & Infrastructure

    • Basic Middleware
    • Custom Middleware
    • Parameterized Middleware
    • Response Compression
    • Request Decompression
    • CORS (Cross-Origin Resource Sharing)
    • Static Files
    • Rate Limiting
    • Configuration Files
  • Security & Access

    • Authentication and Authorization
  • Reliability & Observability

    • Global Error Handling
    • Tracing & Logging
    • Request cancellation
  • Protocols & Realtime

    • HTTP/1 and HTTP/2
    • HTTPS
    • WebSockets
    • Server-Sent Events (SSE)
  • Advanced Patterns

    • Dependency Injection
    • Custom Handling of HEAD, OPTIONS, and TRACE Methods

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 on ctx and next are required.
  • attach() is intended for reusable, parameterized middleware types — typically structs that implement the Middleware trait.

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()
    }
}
Last Updated: 4/6/26, 1:04 PM
Prev
Custom Middleware
Next
Response Compression