FastAPI Router入門:コードを整理する第一歩

はい、FastAPIのルーター(APIRouter)に関する詳細な入門記事を作成します。コードの整理を主眼に置き、約5000語のボリュームで記述します。


FastAPI Router入門:コードを整理する第一歩

FastAPIは、その高速性、非同期サポート、そしてPydanticによるデータ検証・シリアライゼーション、そしてOpenAPI標準に基づいた自動ドキュメント生成機能により、現代のWeb API開発において非常に人気があります。これらの強力な機能のおかげで、開発者は短時間で堅牢でメンテナンス性の高いAPIを構築することができます。

しかし、どんなフレームワークを使っていても、アプリケーションが成長するにつれてコードの管理は重要な課題となります。最初は数個のエンドポイントから始まったAPIも、時間が経つにつれて数十、数百といったエンドポイントを持つようになり、一つのファイルに全てのルーティング定義を書いてしまうと、すぐにコードが読みにくく、保守が困難になってしまいます。

このような問題を解決し、アプリケーションを整理された状態に保つためのFastAPIの強力な機能が APIRouter です。この記事では、FastAPIのAPIRouterがなぜ必要で、どのように使い、そしてそれを使ってどのようにコードを整理していくのかを、詳細なコード例を交えながら徹底的に解説します。FastAPIアプリケーションの成長を見据え、より良いコード設計を目指すための一歩を踏み出しましょう。

1. アプリケーションが大きくなるにつれて直面する問題

FastAPIの基本的な使い方を学んだ方は、おそらく以下のようなコードから始めたのではないでしょうか。

“`python

main.py

from fastapi import FastAPI

app = FastAPI()

@app.get(“/”)
async def read_root():
return {“message”: “Welcome to my API”}

@app.get(“/items/{item_id}”)
async def read_item(item_id: int, q: str | None = None):
return {“item_id”: item_id, “q”: q}

@app.post(“/items/”)
async def create_item(item: dict):
return {“item”: item}

… その他、users, orders, products などのエンドポイントがここに追加されていく …

“`

このコードは、数個のエンドポイントしかないうちは全く問題ありません。シンプルで分かりやすく、全ての定義がmain.pyという一つのファイルにまとまっているため、全体像を把握しやすいです。

しかし、アプリケーションが大きくなり、items関連のエンドポイントだけでなく、usersordersproductscategoriesなど、様々なリソースに関するエンドポイントが増えてくるとどうなるでしょうか?

  • コードの行数が増大する: 全てのエンドポイント定義が1つのファイルに積み重なり、数百、数千行規模になります。
  • 可読性の低下: スクロールが大変になり、特定のエンドポイントを探すのが困難になります。関連性の高いエンドポイント(例: GET /users/, POST /users/, GET /users/{user_id}) がファイルの様々な場所に散らばってしまう可能性もあります。
  • 保守性の低下: 特定のリソースに関連するエンドポイントを修正しようとしても、コード全体の中から該当箇所を見つけ出し、他の部分に影響を与えないように慎重に作業する必要があります。新しいエンドポイントの追加も、ファイルのどこに書くべきか悩ましくなります。
  • コードの重複: 認証や共通の依存関係など、複数のエンドポイントで同じ処理が必要になった場合、それを都度定義するか、関数として切り出すにしても管理が複雑になります。
  • チーム開発での問題: 複数の開発者が同じmain.pyファイルを同時に編集すると、コードマージの際にコンフリクトが発生しやすくなります。

これらの問題は、アプリケーションが少し複雑になっただけで簡単に発生します。これを避けるためには、コードを論理的な単位で分割し、整理する必要があります。FastAPIにおいて、この「コードを整理する第一歩」となるのが APIRouter の導入です。

2. APIRouterとは?なぜコード整理に役立つのか?

FastAPIのAPIRouterは、特定のパスプレフィックス(例えば /users/items)や機能に関連する一群のエンドポイントをまとめて管理するためのクラスです。APIRouterを使うことで、ルーティング定義を複数のファイルに分割し、それぞれが独立した単位として機能するようにできます。

APIRouterがコード整理に役立つ理由は以下の通りです。

  • モジュール化: users関連のエンドポイントはusers_router.pyitems関連はitems_router.pyのように、機能やリソースごとにルーティング定義を別々のファイルに分割できます。これにより、各ファイルが担当する範囲が明確になり、ファイルのサイズも適切に保たれます。
  • 関心の分離: 各ファイルが特定のリソースや機能に関するルーティングのみを担当するため、「関心の分離」が促進されます。開発者は特定の機能に集中して作業できるようになります。
  • 可読性の向上: 分割されたファイルは、それぞれの機能に関連するエンドポイント定義のみを含むため、格段に読みやすくなります。
  • 保守性の向上: 特定の機能に修正や機能追加が必要になった場合、関連するルーターファイルだけを修正すればよくなります。他の部分への影響範囲が限定されるため、安心して作業を進められます。
  • 再利用性: 定義したルーターは、異なるFastAPIアプリケーションで再利用することも可能です(ただし、依存関係などに注意は必要です)。
  • チーム開発の効率化: 開発者はそれぞれ異なるルーターファイルを担当できるため、同じファイルを同時に編集することによるコンフリクトを減らせます。

要するに、APIRouterは、アプリケーションのルーティング構造を論理的なチャンクに分割し、それぞれを独立したファイルやモジュールとして管理するための「コンテナ」のような役割を果たします。

3. APIRouterの基本的な使い方

それでは、具体的にAPIRouterを使ってコードを整理する方法を見ていきましょう。
まずは、先ほどのmain.pyの例を、items関連のエンドポイントを別ファイルに分割する形で書き換えてみます。

元のコード (main.py)

“`python

main.py (分割前)

from fastapi import FastAPI

… (後でroutersからのimportを追加) …

app = FastAPI()

@app.get(“/”)
async def read_root():
return {“message”: “Welcome to my API”}

@app.get(“/items/{item_id}”) # この行から下の items 関連を移動
async def read_item(item_id: int, q: str | None = None):
return {“item_id”: item_id, “q”: q}

@app.post(“/items/”)
async def create_item(item: dict):
return {“item”: item}

… users, orders などのエンドポイントがもしあれば、それもここに混在 …

“`

分割後のコード

まず、items関連のエンドポイントを定義するための新しいファイルを作成します。例えば、routersというディレクトリを作成し、その中にitems.pyというファイルを作成するのが一般的です。

.
├── main.py
└── routers/
└── items.py

routers/items.py

“`python

routers/items.py

from fastapi import APIRouter, Depends, HTTPException

依存関係(もしあれば)。ここでは例として簡単なものを定義

async def get_current_user():
# 実際には認証ロジックが入ります
return {“username”: “johndoe”}

APIRouterのインスタンスを作成

prefix=”/items” を指定することで、このルーター内の全てのエンドポイントパスに “/items” が付加されます。

tags=[“items”] を指定することで、Swagger UI でこのルーターのエンドポイントが “items” というタグでグループ化されます。

router = APIRouter(
prefix=”/items”,
tags=[“items”],
dependencies=[Depends(get_current_user)], # このルーターの全てのエンドポイントに適用される依存関係
responses={404: {“description”: “Not found”}}, # このルーターの全てのエンドポイントに適用されるレスポンス定義
)

ルーターを使ってエンドポイントを定義

パスは prefix で指定した “/items” の後続部分になります。

例: @router.get(“/”) の場合、実際のエンドポイントは “/items/” になります。

例: @router.get(“/{item_id}”) の場合、実際のエンドポイントは “/items/{item_id}” になります。

@router.get(“/”)
async def read_items():
# /items/ に対する GET リクエストのハンドラー
# この関数は routers/items.py 内にあるため、items 関連のロジックやデータアクセス処理などをここに記述できます。
return [{“item_id”: “Foo”}, {“item_id”: “Bar”}]

@router.get(“/{item_id}”)
async def read_item(item_id: int, q: str | None = None):
# /items/{item_id} に対する GET リクエストのハンドラー
# パスパラメータやクエリパラメータも通常の FastAPi エンドポイントと同じように定義できます。
return {“item_id”: item_id, “q”: q}

@router.post(“/”)
async def create_item(item: dict):
# /items/ に対する POST リクエストのハンドラー
return {“item”: item}

このファイルには items 関連のエンドポイント定義のみが含まれています。

例えば、ユーザー関連のエンドポイントは別の users.py に記述する、といった形で整理が進みます。

“`

新しいファイルrouters/items.pyでは、以下のことを行っています。

  1. APIRouterをインポートします。
  2. APIRouter()のインスタンスを作成し、routerという変数に代入します。ここでは、prefix="/items"tags=["items"]という引数を渡しています。これについては後述します。
  3. FastAPIアプリケーションのインスタンス(app)ではなく、このrouterインスタンスに対して@router.get()@router.post()といったデコレーターを使ってエンドポイントを定義します。パスは、prefixで指定した/itemsに続く部分になります。

次に、このルーターをメインのFastAPIアプリケーションに「インクルード」します。

main.py (分割後)

“`python

main.py (分割後)

from fastapi import FastAPI

routers ディレクトリから items.py ファイルをインポートし、その中の router インスタンスを取得します。

プロジェクトの構造によって、相対インポート (.routers.items) や絶対インポート (routers.items) を使い分けます。

例: プロジェクトルートに main.py があり、その下に routers ディレクトリがある場合、以下のように記述できます。

from routers import items # routers/items.py をインポート

または from .routers import items # main.py がパッケージの一部の場合など

items モジュールから router インスタンスを取得

items_router = items.router

FastAPIアプリケーションのインスタンスを作成

app = FastAPI()

基本的なルートエンドポイント

@app.get(“/”)
async def read_root():
return {“message”: “Welcome to my API”}

定義したルーターをメインアプリケーションにインクルードします。

これにより、items_router で定義された全てのエンドポイントがアプリケーションに追加されます。

app.include_router(items_router)

もし他のルーター(例: users_router)があれば、それも同様にインクルードします。

from routers import users

app.include_router(users.router)

これで、アプリケーションは以下のエンドポイントを認識します。

GET /

GET /items/

GET /items/{item_id}

POST /items/

“`

main.pyでは、以下のことを行っています。

  1. routers/items.pyをインポートし、その中のrouterインスタンスを取得します。
  2. FastAPIアプリケーションのインスタンスを作成します(これは変わりません)。
  3. app.include_router()メソッドを使って、作成したitems_routerをアプリケーションに組み込みます。

これで、/items/および/items/{item_id}へのリクエストは、routers/items.pyで定義されたハンドラー関数によって処理されるようになります。

このシンプルな例から、APIRouterを使うことで、特定のリソース(この場合はitems)に関連する全てのエンドポイント定義を一つのファイルにまとめることができるのが分かります。main.pyは非常にシンプルになり、アプリケーション全体の構成を把握しやすくなりました。

4. APIRouterの高度な機能とオプション

APIRouterは基本的なルーティング定義の分割だけでなく、共通設定を一括で適用するための便利なオプションを多数持っています。これらを活用することで、さらにコードを効率的に整理し、共通処理の重複を減らすことができます。

APIRouterのコンストラクタには、以下のような引数を渡すことができます。

python
router = APIRouter(
prefix: str | None = None,
tags: list[str | Enum] | None = None,
dependencies: Sequence[params.Depends] | None = None,
default_response_class: type[Response] = Default(JSONResponse),
responses: dict[int | str, dict[str, Any]] | None = None,
callbacks: list[APIRoute] | None = None,
deprecated: bool | None = None,
include_in_schema: bool = True,
generate_unique_id_function: Callable[[APIRoute], str] = Default(generate_unique_id),
# 他にもいくつか引数がありますが、主要なものは上記です
)

これらのうち、コード整理や共通設定の適用に特に役立つ重要なオプションについて詳しく見ていきましょう。

4.1. prefix オプション

prefixオプションは、そのルーターで定義される全てのエンドポイントパスの先頭に、指定した文字列を自動的に付加するためのものです。これは、特定のリソースに関する全てのエンドポイントを、例えば /users/items のような共通のパスで始めたい場合に非常に便利です。

例:

“`python

routers/users.py

from fastapi import APIRouter

prefix=”/users” と設定

router = APIRouter(
prefix=”/users”,
tags=[“users”] # タグも一緒に設定することが多いです
)

@router.get(“/”) # 実際のエンドポイント: GET /users/
async def read_users():
return [{“username”: “Rick”}, {“username”: “Morty”}]

@router.get(“/{user_id}”) # 実際のエンドポイント: GET /users/{user_id}
async def read_user(user_id: int):
return {“user_id”: user_id}

@router.post(“/”) # 実際のエンドポイント: POST /users/
async def create_user(user: dict):
return {“user”: user}
“`

このusers_routermain.pyでインクルードすると、/users//users/{user_id}といったパスでアクセス可能になります。

“`python

main.py

from fastapi import FastAPI
from routers import items, users # items ルーターと users ルーターをインポート

app = FastAPI()

@app.get(“/”) # GET /
async def read_root():
return {“message”: “Welcome to my API”}

items ルーターをインクルード (prefix=”/items” が items.py で設定されていると仮定)

app.include_router(items.router) # エンドポイント: /items/, /items/{item_id}, POST /items/ など

users ルーターをインクルード (prefix=”/users” が users.py で設定されていると仮定)

app.include_router(users.router) # エンドポイント: /users/, /users/{user_id}, POST /users/ など

アプリケーション全体のエンドポイント:

GET /

GET /items/

GET /items/{item_id}

POST /items/

GET /users/

GET /users/{user_id}

POST /users/

“`

prefixを使うことで、各ルーターファイル内のパス定義をシンプルに保ちつつ、アプリケーション全体で一貫性のあるURL構造を簡単に実現できます。これはAPI設計において非常に重要な点です。また、APIのバージョニング(例: /v1/users, /v2/users)を行う際にも、バージョンごとにルーターを作成し、異なるprefixを設定するという方法がよく取られます。

4.2. tags オプション

tagsオプションは、そのルーターで定義される全てのエンドポイントに共通のタグを設定するためのものです。このタグは、FastAPIが自動生成するOpenAPIドキュメント(Swagger UIやReDoc)において、関連するエンドポイントをグループ化するために使用されます。

例:

“`python

routers/products.py

from fastapi import APIRouter

tags=[“products”] と設定

router = APIRouter(
prefix=”/products”,
tags=[“products”]
)

@router.get(“/”) # Swagger UI で “products” グループの下に表示される
async def read_products():
return [{“product_id”: “Laptop”}, {“product_id”: “Mouse”}]

@router.get(“/{product_id}”) # Swagger UI で “products” グループの下に表示される
async def read_product(product_id: str):
return {“product_id”: product_id}
“`

main.pyでこのルーターをインクルードすると、Swagger UIでは以下のように表示されます(簡略化)。

“`
OpenAPI schema: /openapi.json
Swagger UI: /docs
ReDoc: /redoc

tags:
– name: items

– name: users

– name: products

paths:
/items/:
get:
tags:
– items

/items/{item_id}:
get:
tags:
– items

/users/:
get:
tags:
– users

/products/:
get:
tags:
– products


“`

tagsを使うことで、APIドキュメントが非常に整理され、利用者がAPIの機能やエンドポイントを把握しやすくなります。これはAPIの使いやすさに直結する重要な設定です。

4.3. dependencies オプション

dependenciesオプションは、そのルーターで定義される全てのエンドポイントに共通の依存関係(FastAPIのDependency Injection機能)を設定するためのものです。これは、特定のルーター内の全てのエンドポイントで認証が必要な場合や、共通のクエリパラメータを処理する場合などに非常に強力な機能です。

例:

ここでは、簡単な認証を模倣した依存関係を考えます。実際にはトークン検証などを行うことが多いですが、ここではユーザー情報を返すだけの例とします。

“`python

dependencies.py (共通の依存関係を定義するファイル)

from fastapi import Header, HTTPException

async def get_current_user(x_token: str = Header()):
# ここでトークン検証などの認証ロジックを実行
if x_token != “fake-super-secret-token”:
raise HTTPException(status_code=400, detail=”Invalid X-Token header”)
return {“username”: “authenticated_user”}
“`

そして、この依存関係をルーター全体に適用します。

“`python

routers/protected_items.py

from fastapi import APIRouter, Depends, HTTPException

共通の依存関係をインポート

from ..dependencies import get_current_user # プロジェクト構造に合わせてインポートパスを変更

dependencies=[Depends(get_current_user)] と設定

router = APIRouter(
prefix=”/protected-items”,
tags=[“protected items”],
dependencies=[Depends(get_current_user)] # このルーター内の全てのエンドポイントに get_current_user が適用される
)

このエンドポイントにアクセスするには、get_current_user の認証を通過する必要がある

@router.get(“/”)
async def read_protected_items():
return [{“item_id”: “secret Foo”}, {“item_id”: “secret Bar”}]

このエンドポイントも get_current_user の認証を通過する必要がある

@router.get(“/{item_id}”)
async def read_protected_item(item_id: str):
return {“item_id”: item_id}
“`

main.pyでこのルーターをインクルードします。

“`python

main.py

from fastapi import FastAPI
from routers import protected_items # protected_items ルーターをインポート

依存関係を定義したファイルもインポートしておくと分かりやすい

from dependencies import get_current_user # インポートしておかなくても、ルーターで参照できていればOK

app = FastAPI()

@app.get(“/”)
async def read_root():
return {“message”: “Welcome to my API”}

保護されたアイテム ルーターをインクルード

app.include_router(protected_items.router)
“`

この設定により、/protected-items//protected-items/{item_id} へのリクエストは、その前にget_current_user依存関係が実行されます。もし認証に失敗した場合、HTTPExceptionが送出され、エンドポイントのハンドラー関数は実行されません。

個々のエンドポイントにdependencies=[Depends(get_current_user)]を繰り返し書く必要がなくなり、コードの重複が削減され、ルーター全体で統一的な認証や前処理を適用できるため、非常にメンテナンス性が向上します。

4.4. responses オプション

responsesオプションは、そのルーターで定義される全てのエンドポイントに共通のレスポンス定義を設定するためのものです。これは、例えば特定のルーター内の全てのエンドポイントで共通のエラーレスポンス(例: 404 Not Found)がある場合に便利です。

例:

“`python

routers/resource_specific.py

from fastapi import APIRouter, HTTPException, status

router = APIRouter(
prefix=”/resources”,
tags=[“resources”],
# 共通のレスポンス定義を設定
responses={
404: {“description”: “Resource not found”},
401: {“description”: “Authentication failed”} # 例として認証失敗も追加
}
)

このエンドポイントには、ルーターで定義した 404, 401 レスポンス定義が適用される

@router.get(“/{resource_id}”)
async def read_resource(resource_id: str):
# 特定のリソースが見つからない場合
if resource_id == “non_existent”:
# ここで HTTPException を送出すると、Swagger UI で 404 レスポンスが期待されることが示唆される
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=”Resource not found”)

# 認証が必要な場合の例 (dependencies で設定するのが一般的だが、ここでは responses の例として)
# if some_auth_check_fails():
#    raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication failed")

return {"resource_id": resource_id}

@router.post(“/”)
async def create_resource(resource: dict):
# この POST エンドポイントにも、ルーターで定義した 404, 401 レスポンス定義が適用される
return {“created_resource”: resource}
“`

このルーターをインクルードすると、Swagger UIでは/resources/{resource_id}POST /resources/のエンドポイントに対して、共通の404および401レスポンスが定義されていることが示されます。

これにより、APIドキュメントの網羅性を高めつつ、個々のエンドポイントで同じエラーレスポンスを繰り返し記述する手間を省くことができます。

4.5. その他のオプション

  • default_response_class: そのルーター内のエンドポイントで使用されるデフォルトのレスポンスクラスを変更できます(例: HTMLResponse, ORJSONResponseなど)。
  • callbacks: WebSocketなどのコールバックエンドポイントを定義できます。
  • deprecated: ルーター内の全てのエンドポイントを非推奨としてマークできます。APIのバージョンアップなどで古いエンドポイントを残しつつ、非推奨であることを明示する際に役立ちます。
  • include_in_schema: そのルーター内の全てのエンドポイントをAPIスキーマ(Swagger UIなど)に含めるかどうかを制御できます。内部APIなど、ドキュメントに載せたくない場合にFalseを設定します。

これらのオプションを適切に活用することで、APIRouterをさらに強力なコード整理・共通設定適用ツールとして利用できます。

5. アプリケーション構造の例

APIRouterを導入することで、FastAPIアプリケーションのファイル構造をより整理されたものにすることができます。ここでは、APIRouterを使った一般的なアプリケーション構造の例をいくつか紹介します。

例1: 複数のルーターを持つ基本的な構造

.
├── main.py
└── routers/
├── __init__.py # routers ディレクトリを Python パッケージとして認識させるため (空ファイルでOK)
├── items.py # /items 関連のルーター定義
└── users.py # /users 関連のルーター定義

この構造では、main.pyはFastAPIアプリケーションインスタンスの作成と、各ルーターのインクルードのみを行います。具体的なエンドポイント定義は全てrouters/ディレクトリ内の各ファイルに委譲されます。

main.py:
“`python
from fastapi import FastAPI
from routers import items, users # routers パッケージ内の modules をインポート

app = FastAPI()

@app.get(“/”)
async def read_root():
return {“message”: “Welcome to my API”}

app.include_router(items.router) # routers/items.py で定義された router をインクルード
app.include_router(users.router) # routers/users.py で定義された router をインクルード
“`

routers/items.py:
“`python
from fastapi import APIRouter

必要に応じてモデル、データベースアクセス関数などをインポート

router = APIRouter(
prefix=”/items”,
tags=[“items”]
)

@router.get(“/”)
async def read_items():
# items 関連のロジック
return [{“item_id”: “Foo”}, {“item_id”: “Bar”}]

… 他の items エンドポイント …

“`

routers/users.py:
“`python
from fastapi import APIRouter

必要に応じてモデル、データベースアクセス関数などをインポート

router = APIRouter(
prefix=”/users”,
tags=[“users”]
)

@router.get(“/”)
async def read_users():
# users 関連のロジック
return [{“username”: “Rick”}, {“username”: “Morty”}]

… 他の users エンドポイント …

“`

この構造は、APIの規模が中程度になるまで非常に有効です。各ファイルが比較的短くなり、特定の機能に関連するコードを見つけやすくなります。

例2: サービス層などを導入したより進んだ構造

アプリケーションがさらに複雑になると、ルーティング定義だけでなく、ビジネスロジックやデータアクセスロジックも分割・整理する必要が出てきます。その場合、以下のような構造を取ることが多いです。

.
├── main.py
├── routers/ # ルーティング定義
│ ├── __init__.py
│ ├── items.py
│ └── users.py
├── services/ # ビジネスロジック層
│ ├── __init__.py
│ ├── item_service.py
│ └── user_service.py
├── models/ # Pydantic モデル、SQLAlchemy モデルなど
│ ├── __init__.py
│ ├── item.py
│ └── user.py
└── database/ # データベース関連のコード (セッション管理など)
└── database.py

この構造におけるrouters/items.pyは、ルーティングの定義と、サービス層への呼び出し、レスポンスの整形などが主な役割になります。

routers/items.py:
“`python
from fastapi import APIRouter, Depends, HTTPException, status
from typing import List
from sqlalchemy.orm import Session # 例として SQLAlchemy を使用

models, services, database モジュールから必要なものをインポート

from .. import models, schemas # models ディレクトリと schemas ディレクトリを仮定
from ..services import item_service
from ..database import get_db # データベースセッションを取得する依存関係

router = APIRouter(
prefix=”/items”,
tags=[“items”]
)

@router.get(“/”, response_model=List[schemas.Item]) # Pydantic モデルでレスポンスを検証
async def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
# ルーターはリクエストを受け取り、サービス層に処理を依頼する
items = item_service.get_items(db, skip=skip, limit=limit)
return items # サービス層から受け取ったデータをそのまま(あるいは整形して)返す

@router.post(“/”, response_model=schemas.Item)
async def create_item(item: schemas.ItemCreate, db: Session = Depends(get_db)):
# リクエストボディの検証は Pydantic (schemas.ItemCreate) が行う
return item_service.create_item(db, item=item)

@router.get(“/{item_id}”, response_model=schemas.Item)
async def read_item(item_id: int, db: Session = Depends(get_db)):
item = item_service.get_item(db, item_id=item_id)
if item is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=”Item not found”)
return item

… 他の items エンドポイント …

“`

この構造では、ルーターは薄い層となり、複雑なビジネスロジックはサービス層に切り出されます。これにより、コードの責任範囲がさらに明確になり、テストもしやすくなります。APIRouterはこの構造においても、ルーティングに関する部分をカプセル化する重要な役割を果たします。

プロジェクトの規模や複雑さに応じて、このような構造を採用することで、よりスケーラブルで保守性の高いアプリケーションを構築できます。APIRouterは、この階層化された構造の最上位層であるルーティング層を整理するための基盤となります。

6. 実践的な利用例

APIRouterは、アプリケーション構造の整理以外にも、様々な実践的なシナリオで役立ちます。

6.1. APIのバージョニング

APIは時間とともに進化しますが、既存のクライアントとの互換性を維持するために、古いバージョンのAPIエンドポイントを残しておく必要が生じることがあります。APIRouterを使うと、APIのバージョンごとにルーターを分けることで、これを効果的に管理できます。

.
├── main.py
└── routers/
├── __init__.py
├── v1/ # v1 API ルーター
│ ├── __init__.py
│ ├── items.py
│ └── users.py
└── v2/ # v2 API ルーター
├── __init__.py
├── items.py # v2 仕様の items エンドポイント
└── users.py # v2 仕様の users エンドポイント

routers/v1/items.py:
“`python
from fastapi import APIRouter

router = APIRouter(
prefix=”/v1/items”, # v1 のプレフィックス
tags=[“v1”, “items”]
)

@router.get(“/”)
async def read_items_v1():
return [{“id”: 1, “name”: “Foo”}, {“id”: 2, “name”: “Bar”}] # v1 レスポンス形式
“`

routers/v2/items.py:
“`python
from fastapi import APIRouter

router = APIRouter(
prefix=”/v2/items”, # v2 のプレフィックス
tags=[“v2”, “items”]
)

@router.get(“/”)
async def read_items_v2():
return [{“item_id”: 1, “item_name”: “Foo”}, {“item_id”: 2, “item_name”: “Bar”}] # v2 レスポンス形式
“`

main.py:
“`python
from fastapi import FastAPI
from routers.v1 import items as items_v1 # v1 の items ルーターをインポート
from routers.v2 import items as items_v2 # v2 の items ルーターをインポート

… users など他のルーターも同様にインポート …

app = FastAPI()

app.include_router(items_v1.router) # /v1/items/…
app.include_router(items_v2.router) # /v2/items/…

… users ルーターもインクルード …

“`

このようにすることで、/v1/items/v2/itemsという異なるパスで、同じ概念のリソースに対して異なるバージョンのAPIエンドポイントを提供できます。新しいバージョンの開発は古いバージョンに影響を与えずに進められます。

6.2. ロールベースのアクセス制御

特定のユーザーロール(例: admin, regular user)のみがアクセスできるエンドポイント群がある場合、それらを一つのルーターにまとめ、共通の依存関係としてロールチェック関数を設定できます。

“`python

dependencies.py

from fastapi import Depends, HTTPException, Header, status

async def verify_admin_role(x_user_role: str = Header()):
# 実際にはデータベースなどを見てユーザーのロールを確認
if x_user_role != “admin”:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=”Operation not permitted for this role”
)
return x_user_role

async def verify_regular_role(x_user_role: str = Header()):
if x_user_role not in [“admin”, “regular”]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=”Operation not permitted for this role”
)
return x_user_role
“`

“`python

routers/admin.py

from fastapi import APIRouter, Depends
from ..dependencies import verify_admin_role # admin ロールチェック依存関係をインポート

router = APIRouter(
prefix=”/admin”,
tags=[“admin”],
dependencies=[Depends(verify_admin_role)] # このルーター内の全てのエンドポイントは admin ロールが必要
)

@router.get(“/dashboard/”) # /admin/dashboard/
async def read_admin_dashboard():
return {“message”: “Admin dashboard data”}

@router.post(“/users/”) # /admin/users/
async def create_admin_user(user: dict):
return {“message”: “Admin user created”}

このルーター内の他のエンドポイントも全て admin ロールが必要

“`

“`python

routers/users.py (regular user もアクセスできる部分)

from fastapi import APIRouter, Depends
from ..dependencies import verify_regular_role # regular/admin ロールチェック依存関係をインポート

router = APIRouter(
prefix=”/users”,
tags=[“users”],
dependencies=[Depends(verify_regular_role)] # このルーター内の全てのエンドポイントは regular または admin ロールが必要
)

@router.get(“/me/”) # /users/me/
async def read_users_me():
# ログインユーザー自身の情報を返す (ここではシンプル化)
return {“username”: “current_user”}

… 他の users エンドポイント …

“`

main.py:
“`python
from fastapi import FastAPI
from routers import admin, users

app = FastAPI()

app.include_router(admin.router) # /admin/… は admin ロールが必要
app.include_router(users.router) # /users/… は regular または admin ロールが必要

公開エンドポイント

@app.get(“/”)
async def read_root():
return {“message”: “Welcome to my API”}
“`

このように、役割や権限レベルごとにルーターを分割し、共通の依存関係を設定することで、アクセス制御ロジックを集中管理し、コードの重複を排除できます。

6.3. テストの容易化

APIRouterでコードを機能ごとに分割すると、各ルーターを個別にテストしやすくなります。テストコードは、特定のルーターのエンドポイントのみを対象とすることができ、テスト対象の範囲が明確になります。

例えば、routers/items.pyで定義されたルーターをテストする場合、FastAPIのTestClientを使って、そのルーターだけを含むシンプルなアプリケーションを作成し、テストを実行できます。

“`python

tests/test_items_router.py

from fastapi.testclient import TestClient
from fastapi import FastAPI
from routers.items import router as items_router # テストしたいルーターをインポート

テスト用に、テスト対象のルーターだけを含む FastAPI アプリケーションを作成

app = FastAPI()
app.include_router(items_router)

TestClient を作成

client = TestClient(app)

テスト関数

def test_read_items():
# テストしたいエンドポイントにリクエストを送信
response = client.get(“/items/”)
# レスポンスのステータスコードとボディを検証
assert response.status_code == 200
assert response.json() == [{“item_id”: “Foo”}, {“item_id”: “Bar”}]

def test_read_item():
item_id = “Baz”
response = client.get(f”/items/{item_id}”)
assert response.status_code == 200
assert response.json() == {“item_id”: item_id, “q”: None}

… 他の items エンドポイントのテスト …

“`

このように、ルーター単位でテストコードを作成することで、テストの構成が分かりやすくなり、特定の機能に対するテストの実行やデバッグが容易になります。これは、特に大規模なアプリケーション開発において、品質を維持するために非常に重要です。

7. ベストプラクティス

APIRouterを効果的に活用するためのいくつかのベストプラクティスを紹介します。

  1. 論理的な単位で分割する: ルーターは、リソース(users, items, ordersなど)や機能(auth, adminなど)といった論理的な単位で分割しましょう。関連性の高いエンドポイントを一つのルーターにまとめることで、コードの可読性と保守性が向上します。
  2. 適切なファイル/ディレクトリ構造を採用する: ルーターを格納するための専用のディレクトリ(例: routers/)を作成し、その中に各ルーターファイルを配置するのが一般的です。アプリケーションの規模に応じて、さらにサブディレクトリを作成することも検討しましょう(例: routers/v1/, routers/admin/など)。
  3. prefixtagsを積極的に利用する: prefixはURL構造の一貫性を保ち、tagsはAPIドキュメントを整理するために非常に役立ちます。ルーターを作成する際は、ほとんどの場合、これらのオプションを設定することを推奨します。
  4. 共通の依存関係はルーターのdependenciesに設定する: 複数のエンドポイントで同じ認証、認可、あるいはデータ取得などの前処理が必要な場合は、それを依存関係として抽出し、ルーターのdependenciesオプションに設定しましょう。これによりコードの重複を減らし、変更を容易にします。
  5. main.pyはシンプルに保つ: main.pyは、FastAPIアプリケーションインスタンスの作成、グローバルな設定(ミドルウェア、イベントハンドラーなど)、そして各ルーターのインクルードといった、アプリケーション全体の構成を記述する役割に徹しましょう。個々のエンドポイント定義は基本的にmain.pyには書かないようにします。
  6. ルーターの命名規則を統一する: 各ルーターファイルをインポートする際に、分かりやすい変数名(例: items_router, users_routerなど)を使用するか、あるいはファイル内で共通の変数名(例: どのルーターファイルでもrouterという変数名を使う)を使用し、インポート時にエイリアスを付ける(from routers import items, usersとして、items.router, users.routerのようにアクセス)など、プロジェクト内で命名規則を統一しましょう。
  7. 長すぎるルーターを避ける: 一つのルーターファイルがあまりに長大になる(数百行を超えるなど)場合は、さらに細かく分割することを検討しましょう。ルーターが担当する範囲が広すぎると、分割するメリットが薄れてしまいます。

8. 注意点とハマりどころ

APIRouterを使う上でいくつか注意しておきたい点や、ハマりやすいポイントがあります。

  1. インポートパス: ルーターファイルをインポートする際のパスは、プロジェクトのファイル構造や、main.pyをどのように実行するか(単なるスクリプトとして実行するか、あるいはPythonパッケージの一部として実行するか)によって変わってきます。ローカル開発環境では動いても、デプロイ環境でインポートエラーになることがあるので注意が必要です。一般的には、プロジェクトルートを基準とした絶対インポート(例: from routers import items)を使用するか、相対インポート(例: from .routers import items)を適切に使用します。プロジェクトをPythonパッケージとして構成し、その中でモジュールとして扱うのが推奨される方法です。
  2. include_routerの呼び出し順序: 基本的に、app.include_router()の呼び出し順序はルーティングに大きな影響を与えません。FastAPIは内部的にルーティングテーブルを構築する際に、より具体的なパスを優先するように並び替えます。例えば、/items/{item_id}/items/meというパスがあった場合、/items/meのような固定パスが/items/{item_id}のようなパスパラメータを含むパスよりも優先されます。ただし、特定のパスに対する複数のHTTPメソッド定義がある場合は、それらは同じルートエントリーに追加されます。また、依存関係や例外ハンドラーなどがルーター全体に設定されている場合、それらはインクルードされた時点でアプリケーションのルーティング定義に追加されます。
  3. 依存関係のスコープ: APIRouterdependenciesオプションで設定した依存関係は、そのルーターにインクルードされた時点で、ルーター内の全てのエンドポイントに適用されます。アプリケーション全体のdependenciesapp=FastAPI(dependencies=[...])のように設定し、ルーターごとの依存関係はルーターコンストラクタに、特定のエンドポイントごとの依存関係は@app.get("/", dependencies=[...])のようにデコレーターに設定します。それぞれのスコープを理解しておくことが重要です。
  4. 同じパスの定義: 同じパス(パスプレフィックスとパスパラメータを含む完全なパス)に対して、同じHTTPメソッドで複数のエンドポイントを定義することはできません。ルーターを分割していると、意図せず同じパスを定義してしまう可能性が減りますが、異なるルーターで同じパスを定義していないか確認する必要があります。例えば、routers/items.pyprefix="/items"とし、@router.get("/")と定義した場合、routers/products.pyprefix="/items"とし、@router.get("/")と定義すると衝突します。prefixを適切に設定することでこれを防ぎます。
  5. ミドルウェアとイベントハンドラー: ミドルウェアや@app.on_event()で登録するようなイベントハンドラー(startup, shutdown)は、通常main.pyのFastAPIアプリケーションインスタンスに対して設定します。これらはアプリケーション全体に適用されるグローバルな設定です。ルーター単位で特定のミドルウェアやイベントが必要な場合は、別の方法を検討するか、あるいはルーター固有の依存関係として処理を切り出す必要があります。

9. APIRouterの内部(少しだけ)

APIRouterがどのように機能しているのか、その内部を少しだけ覗いてみましょう。

FastAPIの内部では、Starletteという別の非同期Webフレームワークが使用されています。FastAPIはStarletteの上に様々な便利な機能(Pydanticによるデータ検証、DI、自動ドキュメントなど)を追加したものです。

StarletteやFastAPIでは、アプリケーションは内部的にルーティングテーブルを持っています。このテーブルは、 incoming request のパスとHTTPメソッドを、対応するエンドポイントのハンドラー関数にマッピングします。

APIRouterオブジェクトも、それ自身の内部的なルーティングテーブルを持っています。@router.get()@router.post()などのデコレーターを使うたびに、そのエンドポイントの情報(パス、HTTPメソッド、ハンドラー関数、設定など)がルーターの内部テーブルに追加されていきます。

そして、app.include_router(router)を呼び出したとき、FastAPIアプリケーションは指定されたルーターの内部ルーティングテーブルを取得し、それを自身のメインのルーティングテーブルにマージします。このマージの際に、ルーターのprefixオプションが考慮され、各エンドポイントのパスにプレフィックスが付加されます。tagsdependenciesresponsesといったルーターレベルの設定も、マージされるエンドポイントの定義に適切に反映されます。

つまり、APIRouterは単なる設定のコンテナであると同時に、ルーティング情報を一時的に保持し、後からメインアプリケーションに結合するための仕組みを提供しているのです。これにより、エンドポイント定義の作成と、それらをアプリケーション全体に組み込むプロセスを分離でき、モジュール化が実現されます。

10. まとめ

この記事では、FastAPIにおけるAPIRouterの役割と使い方について詳しく見てきました。アプリケーションが成長するにつれて直面するコード管理の課題を解決するための強力なツールとして、APIRouterがどのように機能するのかを理解いただけたかと思います。

APIRouterを使うことの主なメリットは以下の通りです。

  • コードのモジュール化と整理: ルーティング定義を機能やリソースごとに分割し、別々のファイルで管理できます。
  • 可読性と保守性の向上: 各ファイルが小さくなり、特定の機能に関するコードを見つけやすくなります。
  • コードの重複排除: prefix, tags, dependenciesなどのオプションを使って、共通設定をルーター全体に一括で適用できます。
  • チーム開発の効率化: 開発者が異なるファイルを並行して作業しやすくなります。
  • テストの容易化: ルーター単位でテストコードを作成しやすくなります。
  • スケーラブルなアプリケーション構造の基盤: サービス層などの他のレイヤーと組み合わせて、より大規模なアプリケーションに適した構造を構築できます。

FastAPIを使い始めたばかりの方も、ある程度経験がある方も、アプリケーションの規模に関わらず、早い段階からAPIRouterを導入することを強く推奨します。それは、コードが複雑になってからのリファクタリングよりも、最初から整理された構造で開発を進める方がはるかに効率的だからです。

APIRouterは、FastAPIでクリーンでメンテナンス性の高いコードを書くための「コードを整理する第一歩」です。この記事で紹介した基本的な使い方や高度な機能、実践的な利用例、そしてベストプラクティスを参考に、あなたのFastAPIプロジェクトをより良いものにしていきましょう。

FastAPIの世界は広く、依存関係注入、SQLAlchemyなどのORM連携、テスト、デプロイなど、学ぶべきことは他にもたくさんありますが、APIRouterをマスターすることは、その学習パスにおいて非常に重要なマイルストーンとなります。

Happy Coding with FastAPI!


コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール