Webフレームワークにおけるミドルウェアの解剖 - 責任連鎖パターンの深掘り
Daniel Hayes
Full-Stack Engineer · Leapcell

はじめに
バックエンド開発の世界では、着信リクエストの処理は、認証、ロギング、データ解析、エラー処理など、多くの独立していながらも順次的な操作を伴います。これらの懸念事項を個々のルートハンドラに手動で織り込むと、複雑で保守不能なコードにつながります。ここでミドルウェアが輝きを放ち、これらの懸念事項を疎結合するための構造化されエレガントなソリューションを提供します。単なる利便性を超えて、ミドルウェアの基盤となるアーキテクチャを理解することは、堅牢でスケーラブルで拡張性の高いWebアプリケーションを構築するために不可欠です。この記事では、Express (Node.js)、Gin (Go)、Axum (Rust) のような人気フレームワークがミドルウェアをどのように実装しているかを分解し、それらが責任連鎖デザインパターンの典型例であることを明らかにします。
コアコンセプトの理解
フレームワーク自体に飛び込む前に、主要な用語の共通理解を確立しましょう。
- ミドルウェア: 通常、Webサーバーとアプリケーションロジック(または別のミドルウェア)の間でリクエストを処理するソフトウェアコンポーネント。コードを実行したり、リクエスト/レスポンスオブジェクトを変更したり、リクエストを終了したり、リクエストをチェーン内の次のミドルウェアに渡したりできます。
 - 責任連鎖パターン: リクエストをハンドラーのチェーンに沿って渡すことができるビヘイビiors(振る舞い)デザインパターン。各ハンドラーは、リクエストを処理するか、チェーン内の次のハンドラーに渡すかを決定します。このパターンは、リクエストの送信者とその受信者との間の疎結合を促進します。
 - リクエスト/レスポンスオブジェクト: 着信クライアントリクエストの詳細(ヘッダー、ボディ、URLなど)と、発信サーバーレスポンス(ステータス、ヘッダー、ボディ)をカプセル化するデータ構造。ミドルウェアは通常、これらのオブジェクトを操作し、場合によっては変更します。
 - 次の関数/ハンドラー: ミドルウェアに提供されるメカニズム(多くの場合、関数またはクロージャ)で、呼び出されると、実行チェーン内の後続のミドルウェアまたは最終ルートハンドラーに制御を委譲します。
 
責任連鎖の実行
ミドルウェアのエレガンスの大部分は、責任連鎖として実装されていることに由来します。各ミドルウェアはチェーン内の「ハンドラー」として機能します。リクエストが到着すると、最初のハンドラーに入ります。このハンドラーはリクエストを処理し、次に次のハンドラーに渡すか、リクエストを完全に処理してレスポンスを送信し、チェーンを終了するかのいずれかを行います。
Express (Node.js)
Express.js は、おそらくその堅牢なミドルウェアシステムで最も広く認識されているフレームワークの1つです。
// シンプルなロギングミドルウェア function logger(req, res, next) { console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`); next(); // 次のミドルウェアまたはルートハンドラーに制御を渡す } // 認証ミドルウェア function authenticate(req, res, next) { const token = req.headers.authorization; if (token === 'Bearer mysecrettoken') { req.user = { id: 1, name: 'Alice' }; // リクエストにユーザー情報を付加 next(); } else { res.status(401).send('Unauthorized'); } } // Expressアプリケーション const express = require('express'); const app = express(); app.use(logger); // ロガーミドルウェアをグローバルに適用 app.use(express.json()); // JSONボディを解析するための組み込みミドルウェア app.get('/protected', authenticate, (req, res) => { // このルートは、authenticateミドルウェアが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'); });
Express では、app.use() がミドルウェアを登録します。 next() 関数は明示的であり、それがないとリクエストサイクルは停止します。この設計は、logger または authenticate 関数がそれぞれハンドラーであり、リクエストを渡すか fulfillment するかを決定する責任連鎖を直接反映しています。
Gin (Go)
Go の人気 HTTP Web フレームワークである Gin も、ミドルウェアパターンを全面的に採用しています。
package main import ( "fmt" "log" "net/http" "time" "github.com/gin-gonic/gin" ) // シンプルなロギングミドルウェア func LoggerMiddleware() gin.HandlerFunc { return func(c *gin.Context) { t := time.Now() // リクエストを処理 c.Next() // 次のミドルウェアまたはルートハンドラーに制御を渡す // リクエスト処理後 latency := time.Since(t) log.Printf("Request -> %s %s %s took %v", c.Request.Method, c.Request.URL.Path, c.ClientIP(), latency) } } // 認証ミドルウェア func AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { token := c.GetHeader("Authorization") if token == "Bearer mysecrettoken" { c.Set("user", "Alice") // コンテキストにユーザー情報を格納 c.Next() } else { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) // AbortWithStatusJSON はチェーンを中止し、レスポンスを送信します } } } func main() { router := gin.Default() // gin.Default() はデフォルトで Logger と Recovery ミドルウェアを含みます router.Use(LoggerMiddleware()) // カスタムロガーを適用 // 特定のルートグループに認証を適用 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") }
Gin では、ミドルウェア関数は gin.HandlerFunc 型です。 c.Next() は Express の next() と同様にチェーンを進めます。 c.AbortWithStatusJSON() は、チェーンの実行を明示的に停止してレスポンスを送信し、チェーンを終了できるハンドラーとしての役割を強化します。
Axum (Rust)
比較的最近の Rust 用 Web フレームワークである Axum は、Tokio エコシステム上に構築され、Rust の型システムを活用して、非常にパフォーマンスが高く型安全なアプリケーションを作成します。そのミドルウェアシステムは tower::Service トレイトを使用して実装されています。
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; // 例:組み込みの Axum/Tower ミドルウェア #[derive(Clone, FromRef)] // AppState から State を派生させるために FromRef を使用 struct AppState {} // シンプルなロギングミドルウェア(デモンストレーション用のカスタム実装) 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; // 次のミドルウェアまたはルートハンドラーに制御を渡す println!( "Response <- {} {} took {:?}", response.status(), response.body().size_hint().exact().unwrap_or(0), start.elapsed() ); response } // 認証ミドルウェア 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" => { // ユーザー情報を付加する(例:拡張機能を使用) req.extensions_mut().insert("Alice".to_string()); // ユーザー情報を格納 Ok(next.run(req).await) } _ => Err(StatusCode::UNAUTHORIZED), // チェーンを停止するためにエラー状態コードを返す } } #[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 を適用 .layer(middleware::from_fn(log_middleware)) // log_middleware をグローバルに適用 .layer(TraceLayer::new_for_http()); // Axum の組み込み 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 は middleware::from_fn を使用して非同期関数をミドルウェアに変換します。 next.run(req).await の呼び出しは、明示的にリクエストをチェーンに渡します。ミドルウェアが Err(auth_middleware の Err(StatusCode::UNAUTHORIZED) など)を返すと、チェーンがショートサーキットされ、それ以降のハンドラーの実行が防止されます。この非同期で結果駆動のアプローチは、高並列環境で責任連鎖パターンを強化します。
応用と利点
責任連鎖パターンは、ミドルウェアによって促進され、数多くの利点を提供します。
- 疎結合: 各ミドルウェアは単一の懸念事項に焦点を当てており、他のミドルウェアとは独立しています。これにより、コードの理解、テスト、保守が容易になります。
 - モジュール性: ミドルウェアコンポーネントは、コアアプリケーションロジックに影響を与えることなく、簡単に追加、削除、または並べ替えることができます。
 - 再利用性: 認証、ロギング、キャッシュなどの一般的な機能は、再利用可能なミドルウェアとしてパッケージ化し、さまざまなルートやアプリケーションに適用できます。
 - 柔軟性: ミドルウェアの実行順序は動的に制御でき、リクエスト処理の細かい制御を可能にします。
 - 拡張性: 新しいミドルウェアを作成するだけで、既存のコードを変更することなく、新しい機能を追加できます。
 
ミドルウェアの一般的なユースケースには、以下のようなものがあります。
- 認証と認可: ユーザーの資格情報と権限を確認します。
 - ロギングと監視: デバッグと分析のためにリクエストの詳細を記録します。
 - データ解析: JSON、URLエンコード、またはマルチパートフォームデータを処理します。
 - エラー処理: エラーを一貫してキャッチおよびフォーマットします。
 - キャッシング: 頻繁にリクエストされるリソースを格納および提供します。
 - CORS(オリジン間リソース共有): ブラウザのセキュリティポリシーを管理します。
 - レート制限: クライアントリクエストを制限することで、悪用を防ぎます。
 
結論
Express、Gin、Axum のような最新の Web フレームワークにおけるミドルウェアは、責任連鎖デザインパターンの堅牢な基盤の上に構築された、強力で遍在する機能です。この根本的な原則を理解することで、開発者はよりモジュール化され、保守可能で、スケーラブルなバックエンドアプリケーションを作成し、複雑なリクエストフローをエレガンスと精度で効果的にオーケストレーションできます。これは、堅牢なソフトウェアアーキテクチャを形成する上で、デザインパターンの永続的な力の証です。

