Read OSS

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/__init__.py#L1-L26

ユーザーが必要とするもの、すなわち FastAPIAPIRouterDependsQueryPathBodyRequestResponseHTTPException はすべて内部モジュールから再エクスポートされています。これは意図的な設計上の判断で、ユーザーはfastapi.routingfastapi.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

同様に、APIRouterstarlette.routing.Routerを、APIRoutestarlette.routing.Routeをそれぞれ拡張しています。

fastapi/routing.py#L811-L843

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: AsyncExitStackMiddlewareExceptionMiddlewareの内側に配置しているのは意図的な設計です。ユーザー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 APIRouteAPIRouter、リクエスト処理パイプライン
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回では、そのフロー全体を追っていきます。