Entmystifizierung von Middleware in Web-Frameworks – Ein tiefer Einblick in die Chain of Responsibility
Daniel Hayes
Full-Stack Engineer · Leapcell

Einleitung
In der Welt der Backend-Entwicklung beinhaltet die Verarbeitung eingehender Anfragen oft eine Reihe von unabhängigen, aber sequenziellen Operationen: Authentifizierung, Protokollierung, Datenanalyse, Fehlerbehandlung und vieles mehr. Das manuelle Verweben dieser Anliegen in jeden einzelnen Routen-Handler führt zu verschlungenem, nicht wartbarem Code. Hier glänzt Middleware und bietet eine strukturierte und elegante Lösung, um diese Anliegen zu entkoppeln. Über bloße Bequemlichkeit hinaus ist das Verständnis der zugrunde liegenden Architektur von Middleware entscheidend für den Aufbau robuster, skalierbarer und erweiterbarer Webanwendungen. Dieser Artikel analysiert, wie beliebte Frameworks wie Express (Node.js), Gin (Go) und Axum (Rust) Middleware implementieren und offenbart sie als ein Paradebeispiel für das Chain of Responsibility-Entwurfsmuster.
Kernkonzepte verstehen
Bevor wir uns mit den Frameworks selbst befassen, wollen wir ein gemeinsames Verständnis der Schlüsselbegriffe etablieren:
- Middleware: Eine Softwarekomponente, die typischerweise Anfragen zwischen dem Webserver und der Anwendungslogik (oder einer anderen Middleware) verarbeitet. Sie kann Code ausführen, Anforderungs-/Antwortobjekte ändern, den Anforderungs-/Antwortzyklus beenden oder die Anfrage an die nächste Middleware in der Kette weiterleiten.
 - Chain of Responsibility-Muster: Ein verhaltensbezogenes Entwurfsmuster, das es erlaubt, eine Anfrage entlang einer Kette von Handlern weiterzuleiten. Jeder Handler entscheidet entweder, die Anfrage zu verarbeiten oder sie an den nächsten Handler in der Kette weiterzuleiten. Dieses Muster fördert eine lose Kopplung zwischen dem Sender einer Anfrage und deren Empfängern.
 - Anforderungs-/Antwortobjekt: Datenstrukturen, die die Details der eingehenden Client-Anfrage (Header, Body, URL usw.) und der ausgehenden Server-Antwort (Status, Header, Body) kapseln. Middleware arbeitet typischerweise mit diesen Objekten und modifiziert sie möglicherweise.
 - Next-Funktion/Handler: Ein Mechanismus (oft eine Funktion oder ein Closure), der der Middleware zur Verfügung gestellt wird und bei dessen Aufruf die Kontrolle an die nachfolgende Middleware oder den endgültigen Routen-Handler in der Ausführungskette übergeben wird.
 
Die Chain of Responsibility in Aktion
Die Eleganz von Middleware beruht weitgehend auf ihrer Implementierung als Chain of Responsibility. Jede Middleware fungiert als „Handler“ in der Kette. Wenn eine Anfrage eintrifft, tritt sie in den ersten Handler ein. Dieser Handler verarbeitet die Anfrage und leitet sie dann explizit an den nächsten Handler weiter, oder er bearbeitet die Anfrage vollständig und sendet eine Antwort, wodurch die Kette beendet wird.
Express (Node.js)
Express.js ist vielleicht eines der bekanntesten Frameworks für sein robustes Middleware-System.
// Eine einfache Logging-Middleware function logger(req, res, next) { console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`); next(); // Kontrolle an die nächste Middleware oder den Routen-Handler weitergeben } // Eine Authentifizierungs-Middleware function authenticate(req, res, next) { const token = req.headers.authorization; if (token === 'Bearer mysecrettoken') { req.user = { id: 1, name: 'Alice' }; // Benutzerinformationen an die Anfrage anhängen next(); } else { res.status(401).send('Unauthorized'); } } // Eine Express-Anwendung const express = require('express'); const app = express(); app.use(logger); // Logger-Middleware global anwenden app.use(express.json()); // Eingebaute Middleware zum Parsen von JSON-Bodies app.get('/protected', authenticate, (req, res) => { // Diese Route wird nur erreicht, wenn die Authentifizierungs-Middleware next() aufruft res.json({ message: `Willkommen, ${req.user.name}!`, data: 'Geheime Informationen' }); }); app.get('/', (req, res) => { res.send('Hallo Welt!'); }); app.listen(3000, () => { console.log('Server läuft auf Port 3000'); });
In Express registriert app.use() Middleware. Die Funktion next() ist explizit: Ohne sie stoppt der Anforderungsvorgang. Dieses Design spiegelt direkt die Chain of Responsibility wider, wobei jede logger- oder authenticate-Funktion ein Handler ist, der entscheidet, ob die Anfrage weitergeleitet oder erfüllt werden soll.
Gin (Go)
Gin, ein beliebtes HTTP-Web-Framework für Go, setzt ebenfalls voll auf das Middleware-Muster.
package main import ( "fmt" "log" "net/http" "time" "github.com/gin-gonic/gin" ) // Eine einfache Logging-Middleware func LoggerMiddleware() gin.HandlerFunc { return func(c *gin.Context) { t := time.Now() // Anfrage verarbeiten c.Next() // Kontrolle an die nächste Middleware oder den Routen-Handler weitergeben // Nachdem die Anfrage verarbeitet wurde latency := time.Since(t) log.Printf("Request -> %s %s %s dauerte %v", c.Request.Method, c.Request.URL.Path, c.ClientIP(), latency) } } // Eine Authentifizierungs-Middleware func AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { token := c.GetHeader("Authorization") if token == "Bearer mysecrettoken" { c.Set("user", "Alice") // Benutzerinformationen im Kontext speichern c.Next() } else { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) // AbortWithStatusJSON bricht die Kette ab und sendet eine Antwort } } } func main() { router := gin.Default() // gin.Default() enthält standardmäßig Logger- und Recovery-Middleware router.Use(LoggerMiddleware()) // Unser benutzerdefinierter Logger anwenden // Authentifizierung für eine bestimmte Gruppe von Routen anwenden 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": "Benutzer nicht im Kontext gefunden"}) return } c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("Willkommen, %v!", user), "data": "Geheime Informationen"}) }) } router.GET("/", func(c *gin.Context) { c.String(http.StatusOK, "Hallo Welt!") }) router.Run(":8080") }
In Gin sind Middleware-Funktionen vom Typ gin.HandlerFunc. c.Next() rückt die Kette vorwärts, ähnlich wie bei Express' next(). c.AbortWithStatusJSON() stoppt explizit die Ausführungskette und sendet eine Antwort, was seine Rolle als Handler, der die Anforderung beenden kann, verstärkt.
Axum (Rust)
Axum, ein relativ neues Web-Framework für Rust, baut auf dem Tokio-Ökosystem auf und nutzt das Typsystem von Rust, um hochperformante und typsichere Anwendungen zu erstellen. Sein Middleware-System wird über den tower::Service-Trait implementiert.
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; // Beispiel für eine eingebaute Axum/Tower-Middleware #[derive(Clone, FromRef)] // FromRef zum Ableiten von State von AppState struct AppState {} // Eine einfache Logging-Middleware (benutzerdefinierte Implementierung zur 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; // Kontrolle an die nächste Middleware oder den Routen-Handler weitergeben println!( "Response <- {} {} dauerte {:?}", response.status(), response.body().size_hint().exact(), start.elapsed() ); response } // Eine Authentifizierungs-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" => { // Benutzerinformationen anhängen (z. B. über Erweiterungen) req.extensions_mut().insert("Alice".to_string()); // Benutzerinformationen speichern Ok(next.run(req).await) } _ => Err(StatusCode::UNAUTHORIZED), // Einen Fehler-Statuscode zurückgeben, um die Kette zu stoppen } } #[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)) // auth_middleware auf diese und nachfolgende Routen anwenden .layer(middleware::from_fn(log_middleware)) // log_middleware global anwenden .layer(TraceLayer::new_for_http()); // Axums eingebaute TraceLayer anwenden 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 { "Hallo, Axum!".to_string() } async fn handler_protected(State(_app_state): State<AppState>, username: axum::extract::Extension<String>) -> String { format!("Willkommen, {}! Geheime Informationen.", username.0) }
Axum verwendet middleware::from_fn, um eine asynchrone Funktion in eine Middleware umzuwandeln. Der Aufruf next.run(req).await leitet die Anfrage explizit die Kette hinunter weiter. Wenn eine Middleware einen Err zurückgibt (wie Err(StatusCode::UNAUTHORIZED) in auth_middleware), wird die Kette unterbrochen, sodass keine weiteren Handler ausgeführt werden. Dieser asynchrone, ergebnisgesteuerte Ansatz stärkt das Chain of Responsibility-Muster in einer hochgradig nebenläufigen Umgebung.
Anwendungen und Vorteile
Das Chain of Responsibility-Muster, das durch Middleware ermöglicht wird, bietet zahlreiche Vorteile:
- Entkopplung: Jede Middleware konzentriert sich auf ein einzelnes Anliegen und ist unabhängig von anderen. Das macht den Code leichter verständlich, testbar und wartbar.
 - Modularität: Middleware-Komponenten können einfach hinzugefügt, entfernt oder neu angeordnet werden, ohne die Kernanwendungslogik zu beeinträchtigen.
 - Wiederverwendbarkeit: Häufige Funktionalitäten wie Authentifizierung, Protokollierung oder Caching können als wiederverwendbare Middleware verpackt und in verschiedenen Routen oder Anwendungen verwendet werden.
 - Flexibilität: Die Reihenfolge der Middleware-Ausführung kann dynamisch gesteuert werden, was eine feingranulare Kontrolle über die Anforderungsverarbeitung ermöglicht.
 - Erweiterbarkeit: Neue Funktionalitäten können hinzugefügt werden, indem einfach neue Middleware erstellt wird, ohne bestehenden Code zu ändern.
 
Zu den gängigen Anwendungsfällen für Middleware gehören:
- Authentifizierung und Autorisierung: Überprüfung von Benutzeranmeldeinformationen und Berechtigungen.
 - Protokollierung und Überwachung: Aufzeichnung von Anfragedetails für Debugging und Analysen.
 - Datenanalyse: Verarbeitung von JSON-, URL-codierten- oder Multipart-Formulardaten.
 - Fehlerbehandlung: Konsistentes Auffangen und Formatieren von Fehlern.
 - Caching: Speichern und Servieren von häufig angeforderten Ressourcen.
 - CORS (Cross-Origin Resource Sharing): Verwaltung von Browser-Sicherheitsrichtlinien.
 - Ratenbegrenzung: Verhinderung von Missbrauch durch Begrenzung von Client-Anfragen.
 
Fazit
Middleware in modernen Web-Frameworks wie Express, Gin und Axum ist ein leistungsfähiges und allgegenwärtiges Merkmal, das auf der robusten Grundlage des Chain of Responsibility-Entwurfsmusters aufbaut. Durch das Verständnis dieses zugrunde liegenden Prinzips können Entwickler modularere, wartbarere und skalierbarere Backend-Anwendungen schreiben und komplexe Anforderungsflüsse elegant und präzise orchestrieren. Es ist ein Beweis für die anhaltende Kraft von Entwurfsmustern bei der Gestaltung robuster Software-Architekturen.