FastAPIのアーキテクチャ:StarletteをうまくExtendする設計思想
前提知識
- ›ASGIプロトコルの基礎知識
- ›Pythonのクラス継承とメソッド解決順序(MRO)の理解
- ›FastAPIを使ったAPI開発の経験
FastAPIのアーキテクチャ:StarletteをうまくExtendする設計思想
FastAPIは「非同期時代のFlask」や「Django REST Frameworkをより高速にしたもの」と表現されることが多いですが、こうした比較はアーキテクチャの本質を捉えきれていません。FastAPIは、ASGIツールキットであるStarletteの上に薄いレイヤーを重ねた存在です。HTTPハンドリング、ルーティング、middlewareを置き換えるのではなく、それらを拡張しています。この関係性を理解することが、デコレータの仕組みからOpenAPIスキーマの生成方法まで、FastAPI全体を理解する鍵になります。
第1回では、StarletteからFastAPIへのクラス階層をたどりながら、middlewareスタックの組み立て方やドキュメント用ルートの登録方法を見ていきます。ここで築く基礎知識は、シリーズ全体を通じて欠かせない土台となります。
パブリックAPIサーフェス
FastAPIのパブリックインターフェースは、驚くほどシンプルです。__init__.py全体はわずか25行の再エクスポートで構成されています。
ユーザーが必要とするもの、すなわち FastAPI、APIRouter、Depends、Query、Path、Body、Request、Response、HTTPException はすべて内部モジュールから再エクスポートされています。これは意図的な設計上の判断で、ユーザーはfastapi.routingやfastapi.dependencies.utilsから直接インポートする必要がありません。複雑な内部実装は、すっきりとした外部インターフェースの裏側に隠されています。
statusはStarletteから直接取り込まれており、FastAPI側でわざわざラップしていません。「Starletteで十分なところはStarletteをそのまま使う」という思想が、コードベース全体に一貫して流れています。
Tip: FastAPIの上にライブラリを構築するなら、同じパターンを踏襲しましょう。公開シンボルは
__init__.pyから再エクスポートすることで、ユーザーはインポートパスを1か所にまとめられます。
クラス階層:Starlette → FastAPI
FastAPIのアーキテクチャの核心は、Starletteのルーティングクラスを拡張する3つの継承チェーンです。
classDiagram
class Starlette {
+middleware_stack
+routes
+build_middleware_stack()
+add_route()
}
class FastAPI {
+router: APIRouter
+openapi_schema
+dependency_overrides
+build_middleware_stack()
+openapi()
+setup()
}
class StarletteRouter["starlette.routing.Router"] {
+routes
+add_route()
+include_router()
}
class APIRouter {
+dependencies
+responses
+callbacks
+add_api_route()
+include_router()
}
class StarletteRoute["starlette.routing.Route"] {
+path
+endpoint
+methods
+app
}
class APIRoute {
+dependant: Dependant
+response_model
+body_field
+response_field
}
Starlette <|-- FastAPI
StarletteRouter <|-- APIRouter
StarletteRoute <|-- APIRoute
FastAPIクラスの宣言はシンプルで、Starletteを継承しています。
fastapi/applications.py#L41-L55
同様に、APIRouterはstarlette.routing.Routerを、APIRouteはstarlette.routing.Routeをそれぞれ拡張しています。
fastapi/routing.py#L1005-L1030
これはラッピングではなく、拡張です。FastAPIはStarletteアプリを内包しているのではなく、FastAPIそのものがStarletteアプリなのです。つまり、Starletteで動作するすべてのmiddleware、ASGIユーティリティ、デプロイ戦略は、FastAPIでもそのまま使えます。
各レイヤーで追加される重要な要素は、型駆動のインテリジェンスです。APIRouteはHTTPマッチングの仕組みを変えるのではなく、Dependantツリー、レスポンスモデルのバリデーション、ボディフィールドの抽出機能を追加します。APIRouterもルートの収集方法を変えるのではなく、依存関係のマージ、タグの伝播、レスポンスの集約機能を追加します。
内部APIRouterとルートの委譲
FastAPI()インスタンスを作成すると、コンストラクタは内部的にAPIRouterを生成し、すべてのルーティング操作をそこに委譲します。
fastapi/applications.py#L982-L997
これはファサードパターンの実装です。app.get("/items")を呼び出すと、そのメソッドはAPIRouterに定義されており、継承チェーンを通じて利用できます。FastAPIクラス自体が主に担うのは、middlewareの組み立て、OpenAPIスキーマの生成、そしてドキュメント用ルートの登録という3つの責務です。
flowchart LR
A["app.get('/items')"] --> B["APIRouter.add_api_route()"]
B --> C["Creates APIRoute"]
C --> D["APIRoute.__init__ builds Dependant tree"]
D --> E["Route added to self.routes"]
dependency_overrides_provider=selfという引数は特に重要です。これによりFastAPIインスタンス自体が依存関係のオーバーライドを管理する権限者として渡され、テスト時のapp.dependency_overridesの動作を実現しています。このメカニズムの詳細は第7回で説明します。
middlewareスタックの組み立て
FastAPIはStarletteのbuild_middleware_stack()をオーバーライドして、AsyncExitStackMiddlewareという重要なmiddlewareを挿入します。組み立てられるスタックは、決まった順序で構成されます。
fastapi/applications.py#L1018-L1066
sequenceDiagram
participant Client
participant SEM as ServerErrorMiddleware
participant UM as User Middleware
participant EM as ExceptionMiddleware
participant AES as AsyncExitStackMiddleware
participant R as Router
Client->>SEM: Request
SEM->>UM: Forward (catches 500s)
UM->>EM: Forward
EM->>AES: Forward (catches HTTPException)
AES->>R: Forward (creates middleware exit stack)
R->>AES: Response
AES->>EM: Response (stack cleanup)
EM->>UM: Response
UM->>SEM: Response
SEM->>Client: Response
middlewareのリストはPythonのリストとして構築され、反転されてからラッピングチェーンとして適用されます。
middleware = (
[Middleware(ServerErrorMiddleware, ...)]
+ self.user_middleware
+ [Middleware(ExceptionMiddleware, ...),
Middleware(AsyncExitStackMiddleware)]
)
AsyncExitStackMiddleware自体はとてもシンプルで、わずか18行です。
fastapi/middleware/asyncexitstack.py#L1-L19
AsyncExitStackを生成してASGIスコープにfastapi_middleware_astackとして保存します。このスタックは主に、リクエスト完了後にアップロードされたファイルを閉じるファイルクリーンアップに使われます。build_middleware_stack()内の詳細なコメントには、このmiddlewareがすべてのユーザーmiddlewareの内側に配置されなければならない理由が説明されています。AnyIOのタスクグループ境界をまたいでcontextvarsのコンテキストを保持するためです。
Tip:
AsyncExitStackMiddlewareをExceptionMiddlewareの内側に配置しているのは意図的な設計です。ユーザーmiddlewareの外側に置いてしまうと、依存関係でセットされたcontextvarsがexitスタックのクリーンアップスコープから見えなくなってしまいます。
setup()メソッド:ドキュメント用ルートの登録
setup()メソッドは、OpenAPI JSONエンドポイント、Swagger UI、ReDocのルートを登録します。
fastapi/applications.py#L1101-L1155
flowchart TD
A["setup()"] --> B{openapi_url set?}
B -->|Yes| C["Register /openapi.json"]
B -->|No| Z["Done"]
C --> D{docs_url set?}
D -->|Yes| E["Register /docs (Swagger UI)"]
E --> F{oauth2_redirect_url set?}
F -->|Yes| G["Register /docs/oauth2-redirect"]
D -->|No| H{redoc_url set?}
F -->|No| H
G --> H
H -->|Yes| I["Register /redoc"]
H -->|No| Z
I --> Z
setup()はFastAPI.__init__()の末尾で呼び出されており、最初のリクエスト時に遅延実行されるわけではありません。ルートは即座に登録されますが、スキーマ自体は遅延生成されます。/openapi.jsonへのリクエストが来たとき、openapi()クロージャがself.openapi()を呼び出し、初回アクセス時にスキーマを生成してキャッシュします。
openapiエンドポイントハンドラはroot_pathの処理も担当します。これはリバースプロキシ背後のアプリにとって重要なASGIの機能です。ASGIスコープにルートパスが設定されており、スキーマのserversリストに含まれていない場合は、先頭に追加されます。
すべてのドキュメント用ルートはinclude_in_schema=Falseで登録されるため、生成されるOpenAPIスペックには含まれません。API仕様書の中に/docsがAPIエンドポイントとして表示されては困りますよね。
ディレクトリマップ
| ファイル | 役割 |
|---|---|
fastapi/__init__.py |
パブリックAPIサーフェス — 25行の再エクスポートファイル |
fastapi/applications.py |
FastAPI(Starlette)クラス — middleware、OpenAPI、セットアップ |
fastapi/routing.py |
APIRoute、APIRouter、リクエスト処理パイプライン |
fastapi/middleware/asyncexitstack.py |
ファイルクリーンアップ用exitスタック — 18行のmiddleware |
fastapi/openapi/docs.py |
Swagger UIおよびReDocのHTML生成 |
fastapi/openapi/utils.py |
ルートからのOpenAPIスキーマ生成 |
次回予告
FastAPIがStarletteの上にどのようにレイヤーを重ねているかを理解できたところで、次に気になるのは「@app.get()で関数をデコレートしたとき、APIRoute.__init__()の中では何が起きているのか?」という点です。そこに本当の魔法があります。FastAPIは関数のシグネチャをイントロスペクトし、すべてのパラメータを解析して、リクエスト時ではなく登録時に依存関係ツリーを構築しています。第2回では、そのフロー全体を追っていきます。