Unpacking Middleware in Web Frameworks - A Chain of Responsibility Deep Dive
Daniel Hayes
Full-Stack Engineer · Leapcell

Introduction
In the world of backend development, handling incoming requests often involves a series of independent yet sequential operations: authentication, logging, data parsing, error handling, and so much more. Manually weaving these concerns into every single route handler leads to tangled, unmaintainable code. This is where middleware shines, offering a structured and elegant solution to decouple these concerns. Beyond mere convenience, understanding middleware's underlying architecture is crucial for building robust, scalable, and extensible web applications. This article will dissect how popular frameworks like Express (Node.js), Gin (Go), and Axum (Rust) implement middleware, revealing it as a quintessential example of the Chain of Responsibility design pattern.
Understanding the Core Concepts
Before we dive into the frameworks themselves, let's establish a common understanding of the key terms:
- Middleware: A software component that typically processes requests between the web server and the application logic (or another middleware). It can execute code, modify request/response objects, end the request-response cycle, or pass the request to the next middleware in the chain.
 - Chain of Responsibility Pattern: A behavioral design pattern that allows a request to be passed along a chain of handlers. Each handler decides either to process the request or to pass it to the next handler in the chain. This pattern promotes loose coupling between the sender of a request and its receivers.
 - Request/Response Object: Data structures that encapsulate the incoming client request details (headers, body, URL, etc.) and the outgoing server response (status, headers, body). Middleware typically operates on and potentially modifies these objects.
 - Next Function/Handler: A mechanism (often a function or a closure) provided to middleware that, when called, relinquishes control to the subsequent middleware or the final route handler in the execution chain.
 
The Chain of Responsibility in Action
The elegance of middleware largely stems from its implementation as a Chain of Responsibility. Each middleware acts as a "handler" in the chain. When a request arrives, it enters the first handler. This handler either processes the request and then explicitly passes it to the next handler, or it might fully handle the request and send a response, thus terminating the chain.
Express (Node.js)
Express.js is perhaps one of the most widely recognized frameworks for its robust middleware system.
// A simple logging middleware function logger(req, res, next) { console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`); next(); // Pass control to the next middleware or route handler } // An authentication middleware function authenticate(req, res, next) { const token = req.headers.authorization; if (token === 'Bearer mysecrettoken') { req.user = { id: 1, name: 'Alice' }; // Attach user info to the request next(); } else { res.status(401).send('Unauthorized'); } } // An Express application const express = require('express'); const app = express(); app.use(logger); // Apply logger middleware globally app.use(express.json()); // Built-in middleware for parsing JSON bodies app.get('/protected', authenticate, (req, res) => { // This route will only be reached if authenticate middleware calls next() res.json({ message: `Welcome, ${req.user.name}!`, data: 'Secret info' }); }); app.get('/', (req, res) => { res.send('Hello World!'); }); app.listen(3000, () => { console.log('Server running on port 3000'); });
In Express, app.use() registers middleware. The next() function is explicit: without it, the request cycle halts. This design directly reflects the Chain of Responsibility, where each logger or authenticate function is a handler, deciding whether to pass the request along or fulfill it.
Gin (Go)
Gin, a popular HTTP web framework for Go, also embraces the middleware pattern wholeheartedly.
package main import ( "fmt" "log" "net/http" "time" "github.com/gin-gonic/gin" ) // A simple logging middleware func LoggerMiddleware() gin.HandlerFunc { return func(c *gin.Context) { t := time.Now() // Process request c.Next() // Pass control to the next middleware or route handler // After request is processed latency := time.Since(t) log.Printf("Request -> %s %s %s took %v", c.Request.Method, c.Request.URL.Path, c.ClientIP(), latency) } } // An authentication middleware func AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { token := c.GetHeader("Authorization") if token == "Bearer mysecrettoken" { c.Set("user", "Alice") // Store user info in context c.Next() } else { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) // AbortWithStatusJSON aborts the chain and sends a response } } } func main() { router := gin.Default() // gin.Default() includes Logger and Recovery middleware by default router.Use(LoggerMiddleware()) // Apply our custom logger // Apply authentication to a specific group of routes protected := router.Group("/protected") protected.Use(AuthMiddleware()) { protected.GET("/", func(c *gin.Context) { user, exists := c.Get("user") if !exists { c.JSON(http.StatusInternalServerError, gin.H{"error": "User not found in context"}) return } c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("Welcome, %v!", user), "data": "Secret info"}) }) } router.GET("/", func(c *gin.Context) { c.String(http.StatusOK, "Hello World!") }) router.Run(":8080") }
In Gin, middleware functions are gin.HandlerFunc types. c.Next() advances the chain, similar to Express's next(). c.AbortWithStatusJSON() explicitly stops the execution chain and sends a response, reinforcing its role as a handler that can terminate the request.
Axum (Rust)
Axum, a relatively new web framework for Rust, builds upon the Tokio ecosystem and leverages Rust's type system to create highly performant and type-safe applications. Its middleware system is implemented using the tower::Service trait.
use axum::{ extract::{FromRef, Request, State}, http::{ header::{AUTHORIZATION, CONTENT_TYPE}, HeaderValue, StatusCode, }, middleware::{self, Next}, response::Response, routing::get, Router, }; use std::time::Instant; use tower_http::{trace::TraceLayer, AuthToken}; // Example of a built-in Axum/Tower middleware #[derive(Clone, FromRef)] // FromRef for deriving State from AppState struct AppState {} // A simple logging middleware (custom implementation for demonstration) async fn log_middleware(req: Request, next: Next) -> Response { let start = Instant::now(); println!("Request -> {} {}", req.method(), req.uri()); let response = next.run(req).await; // Pass control to the next middleware or route handler println!( "Response <- {} {} took {:?}", response.status(), response.body().size_hint().exact(), start.elapsed() ); response } // An authentication middleware async fn auth_middleware(State(_app_state): State<AppState>, mut req: Request, next: Next) -> Result<Response, StatusCode> { let auth_header = req.headers().get(AUTHORIZATION).and_then(|header| header.to_str().ok()); match auth_header { Some(token) if token == "Bearer mysecrettoken" => { // Attach user info (e.g., using extensions) req.extensions_mut().insert("Alice".to_string()); // Store user info Ok(next.run(req).await) } _ => Err(StatusCode::UNAUTHORIZED), // Return an error status code to stop the chain } } #[tokio::main] async fn main() { let app = Router::new() .route("/", get(handler_root)) .route("/protected", get(handler_protected)) .route_layer(middleware::from_fn(auth_middleware)) // Apply auth_middleware to this route and subsequent ones .layer(middleware::from_fn(log_middleware)) // Apply log_middleware globally .layer(TraceLayer::new_for_http()); // Apply Axum's built-in TraceLayer let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); println!("listening on {}", listener.local_addr().unwrap()); axum::serve(listener, app).await.unwrap(); } async fn handler_root() -> String { "Hello, Axum!".to_string() } async fn handler_protected(State(_app_state): State<AppState>, username: axum::extract::Extension<String>) -> String { format!("Welcome, {}! Secret info.", username.0) }
Axum uses middleware::from_fn to convert an async function into a middleware. The next.run(req).await call explicitly passes the request down the chain. If a middleware returns an Err (like Err(StatusCode::UNAUTHORIZED) in auth_middleware), it short-circuits the chain, preventing further handlers from executing. This asynchronous, result-driven approach reinforces the Chain of Responsibility pattern in a highly concurrent environment.
Applications and Benefits
The Chain of Responsibility pattern, facilitated by middleware, offers numerous advantages:
- Decoupling: Each middleware is focused on a single concern, independent of others. This makes code easier to understand, test, and maintain.
 - Modularity: Middleware components can be easily added, removed, or reordered without affecting the core application logic.
 - Reusability: Common functionalities like authentication, logging, or caching can be packaged as reusable middleware and applied across different routes or applications.
 - Flexibility: The order of middleware execution can be dynamically controlled, allowing for fine-grained control over request processing.
 - Extensibility: New functionalities can be added by simply creating new middleware without modifying existing code.
 
Common use cases for middleware include:
- Authentication and Authorization: Verifying user credentials and permissions.
 - Logging and Monitoring: Recording request details for debugging and analytics.
 - Data Parsing: Handling JSON, URL-encoded, or multipart form data.
 - Error Handling: Catching and formatting errors consistently.
 - Caching: Storing and serving frequently requested resources.
 - CORS (Cross-Origin Resource Sharing): Managing browser security policies.
 - Rate Limiting: Preventing abuse by limiting client requests.
 
Conclusion
Middleware in modern web frameworks like Express, Gin, and Axum is a powerful and ubiquitous feature built upon the robust foundation of the Chain of Responsibility design pattern. By understanding this underlying principle, developers can write more modular, maintainable, and scalable backend applications, effectively orchestrating complex request flows with elegance and precision. It's a testament to the enduring power of design patterns in shaping resilient software architectures.