FastAPIでカスタムミドルウェアを構築しAPI制御を強化する
Wenhao Wang
Dev Intern · Leapcell

はじめに
バックエンド開発の世界では、堅牢でスケーラブルなAPIの構築は、エンドポイントの定義だけでは済まされません。アプリケーションが成長するにつれて、ほぼすべての上りリクエストまたは下りレスポンスに対して実行する必要がある繰り返しタスクに遭遇するでしょう。これらのタスクには、認証、ロギング、エラー処理、カスタムヘッダーの追加、さらにはリクエスト/レスポンスボディの変更などが含まれる場合があります。これらの機能は個々のエンドポイントハンドラに散在させることもできますが、このアプローチはすぐにコードの重複、保守性の低下、懸念事項の明確な分離の欠如につながります。まさにここでミドルウェアの概念が輝きます。ミドルウェアは、コアアプリケーションロジックに到達するずっと前、またはロジックが完了した後に、グローバルレベルでリクエストとレスポンスをインターセプトして処理するための強力でエレガントな集中化されたメカニズムを提供します。ミドルウェアを活用することで、APIの制御、柔軟性、およびアーキテクチャのクリーンさを大幅に向上させることができます。この記事では、これらの一般的な課題に対処し、バックエンド開発プロセスを合理化するために、人気のある2つのPython WebフレームワークであるFastAPIとStarletteでカスタムミドルウェアを構築する実践的な側面を掘り下げます。
Webフレームワークにおけるミドルウェアの理解
実装の詳細に入る前に、関連するコアコンセプトについて共通の理解を確立しましょう。
ミドルウェアとは?
その核心において、ミドルウェアは、リクエストとアプリケーションのコアロジックの間、またはアプリケーションのコアロジックとレスポンスの間に介在するソフトウェアです。それを、リクエスト(およびそれに対応するレスポンス)が通過する一連のチェックポイントまたはフィルターと考えてください。各ミドルウェアは特定の操作を実行し、リクエストまたはレスポンスを変更して、チェーンの次のレイヤーに渡すことができます。
StarletteとASGI
FastAPIは、軽量ASGIフレームワークであるStarletteの上に構築されています。ASGI(Asynchronous Server Gateway Interface)は、WSGIの精神的後継者であり、非同期操作を処理するように設計されています。Starlette(したがってFastAPI)のミドルウェアは、本質的に非同期です。これは、ミドルウェア関数がアプリケーションの呼び出しを処理する際にasync
であり、await
操作であることを意味します。
主要なミドルウェアの特性:
- インターセプト: ミドルウェアは、上りリクエストと下りレスポンスの両方をインターセプトできます。
- 実行順序: ミドルウェアの定義順序は非常に重要です。リクエストは上から下へとミドルウェアチェーンを通過し、レスポンスは下から上へと通過します。
- 変更: ミドルウェアは、リクエストヘッダー、ボディ、クエリパラメータ、およびレスポンスステータスコード、ヘッダー、ボディを変更できます。
- ショートサーキット: ミドルウェアは、たとえば、メインアプリケーションロジックに到達せずにエラーレスポンスを返すことによって、リクエスト-レスポンスサイクルを早期に終了させることができます。
カスタムミドルウェアの構築
実践的な例でカスタムミドルウェアの作成方法を見ていきましょう。一般的なミドルウェアのユースケースをいくつかカバーします。
リクエスト処理時間のロギング
各リクエストの処理にかかった時間をログに記録することは、パフォーマンス監視とデバッグに非常に役立つ一般的なユースケースです。
# main.py from fastapi import FastAPI, Request, Response import time import logging # ロギングの設定 logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) app = FastAPI() @app.middleware("http") async def add_process_time_header(request: Request, call_next): start_time = time.time() response = await call_next(request) process_time = time.time() - start_time response.headers["X-Process-Time"] = str(process_time) logger.info(f"Request: {request.method} {request.url.path} processed in {process_time:.4f}s") return response @app.get("/") async def read_root(): return {"message": "Hello FastAPI!"} @app.get("/items/{item_id}") async def read_item(item_id: int): # 処理時間のシミュレーション await asyncio.sleep(0.1) return {"item_id": item_id}
この例では:
async
関数add_process_time_header
を定義します。request: Request
とcall_next
を引数として取ります。call_next
は、ミドルウェアチェーンの「次の」操作を表し、最終的にはルートハンドラにつながります。start_time
を記録し、await call_next(request)
で制御を次のミドルウェアまたはルートハンドラに渡してから、レスポンスを受け取った後にprocess_time
を計算します。- 最後に、カスタムヘッダー
X-Process-Time
をレスポンスに追加し、期間をログに記録します。
これを実行するには、main.py
として保存し、uvicorn main:app --reload
を実行します。
次に、リクエストを行います: curl -v http://localhost:8000/items/123
。レスポンスでX-Process-Time
ヘッダーが表示されるはずです。
カスタム認証ミドルウェア
特定のX-API-Key
ヘッダーが存在し、有効である必要があるシンプルなトークンベースの認証システムがあると想像してください。
# main_auth.py from fastapi import FastAPI, Request, HTTPException, status from starlette.responses import JSONResponse app = FastAPI() SECRET_API_KEY = "supersecretkey" @app.middleware("http") async def api_key_auth_middleware(request: Request, call_next): if request.url.path.startswith("/public"): # パブリックパスへのアクセスはAPIキーなしで許可 response = await call_next(request) return response api_key = request.headers.get("X-API-Key") if not api_key or api_key != SECRET_API_KEY: return JSONResponse( status_code=status.HTTP_401_UNAUTHORIZED, content={"detail": "Unauthorized: Invalid or missing X-API-Key"} ) response = await call_next(request) return response @app.get("/protected") async def protected_route(): return {"message": "Welcome, authorized user!"} @app.get("/public") async def public_route(): return {"message": "This is a public endpoint."}
ここで:
X-API-Key
ヘッダーをチェックします。- それが欠落しているか無効な場合、
401 Unauthorized
ステータスでJSONResponse
を即座に返し、リクエストをショートサーキットさせてprotected_route
に到達しないようにします。 - また、ミドルウェアが条件付きでロジックを適用する方法も示しており、パブリックパスが認証チェックをバイパスできるようにします。
テスト:
curl http://localhost:8000/protected
(Unauthorizedになります)curl -H "X-API-Key: wrongkey" http://localhost:8000/protected
(Unauthorizedになります)curl -H "X-API-Key: supersecretkey" http://localhost:8000/protected
(Authorizedになります)curl http://localhost:8000/public
(APIキーなしでAuthorizedになります)
リクエストまたはレスポンスボディの変更(高度)
FastAPI/StarletteのRequest
オブジェクトは、ボディにアクセスするとそれを消費するため、リクエストボディの変更はトリッキーな場合があります。それを変更するには、通常、ボディを読み取り、変更し、新しいRequest
オブジェクト(またはモックオブジェクト)を作成してから渡す必要があります。レスポンスボディの変更はより簡単です。
タイムスタンプを追加してレスポンスボディを変更する方法を説明しましょう。
# main_transform.py import json from fastapi import FastAPI, Request, Response from starlette.background import BackgroundTask app = FastAPI() @app.middleware("http") async def add_timestamp_to_response(request: Request, call_next): response = await call_next(request) # JSONレスポンスのみを変更 if "application/json" in response.headers.get("content-type", ""): # 生のレスポンスボディを読み取る response_body = b"" async for chunk in response.body_iterator: response_body += chunk # デコード、変更、再エンコード try: data = json.loads(response_body) data["timestamp_utc"] = time.time() modified_body = json.dumps(data).encode("utf-8") # 変更されたボディで新しいResponseオブジェクトを作成 # 元のステータスコードとヘッダーをコピー return Response( content=modified_body, status_code=response.status_code, media_type="application/json", headers=dict(response.headers), background=response.background ) except json.JSONDecodeError: # 有効なJSONでない場合は、元のレスポンスを返すだけ pass return response @app.get("/data") async def get_data(): return {"value": 123, "description": "some data"}
この例はより複雑です:
response.body_iterator
を反復処理して元のレスポンスボディを再構築します。ボディはストリームであり、一度しか読み取れないため、これは重要です。- それをデコードし、
timestamp_utc
フィールドを追加し、再エンコードします。 - 重要なのは、変更されたコンテンツで新しい
Response
オブジェクトを返すことで、元のstatus_code
、media_type
、headers
、およびbackground
タスクをすべて保持することです。これにより、元のレスポンスの完全性が維持され、ボディコンテンツのみが変更されます。
curl http://localhost:8000/data
を使用すると、JSONレスポンスにtimestamp_utc
フィールドが追加されていることがわかります。
アプリケーションシナリオ
カスタムミドルウェアは多数のシナリオで適用できます:
- APIキー/トークン検証: 認証と認可のために示されているように。
- リクエスト/レスポンスロギング: デバッグ、監査、パフォーマンストラッキングのため。
- カスタムヘッダーの挿入: 相関ID、トレース情報、またはセキュリティヘッダーの追加。
- グローバルエラーハンドリング: 特定の例外をグローバルにキャッチし、一貫したエラーレスポンスを返す。
- レート制限: 特定のIPまたはユーザーからのリクエスト数を制限して、乱用を防ぐ。
- CORS管理: FastAPIは組み込みCORSミドルウェアを提供していますが、必要に応じてカスタムで、より詳細なロジックを構築することもできます。
- データ変換: 上りリクエストデータを正規化したり、下りレスポンスデータを強化したりする。
結論
FastAPIとStarletteのカスタムミドルウェアは、Webアプリケーションでクロスに関わる懸念事項を管理するための強力で柔軟なアプローチを提供します。認証、ロギング、データ操作などの一般的な機能を集中化することにより、コードの重複を大幅に削減し、保守性を向上させ、リクエスト-レスポンスライフサイクルに対する細粒度の制御を可能にします。このパターンを活用することで、より堅牢でスケーラブルでアーキテクチャ的にクリーンなAPIを構築できます。