FastAPIのAPIRouterは、エンドポイントをファイルごとに分けて書き、最後にinclude_routerで1つのアプリへまとめるための仕組みで、FlaskのBlueprintに相当するものです。この記事では、FastAPI 0.162.7で小さなAPIを3ファイルに分けながら、prefix・tags・dependenciesの指定、入れ子のルーターで/api/v1と/api/v2を分ける方法、0.137.0で変わった内部構造までをコードで確認します。ルートの順序、末尾スラッシュ、operationIdといった、分割した後に出てくるつまずきの直し方も扱います。
まとめ:FastAPIのAPIRouterで押さえる分割・結合・版管理の結論
ルーターは資源(ユーザー、商品、注文)ごとに1ファイル1つ作り、APIRouter(prefix="/items", tags=["items"])のようにその資源に共通する設定をルーター側に書きます。main.pyはinclude_routerを並べるだけの場所にします。prefixの末尾にスラッシュは付けません。付けると起動時に止まります。
認証のように「そのルーター配下は全部通さない」検査はdependenciesに置き、使う側ごとに認証方式を変えたいときだけinclude_routerの引数で足します。バージョニングは、/api/v1と/api/v2の親ルーターを作り、変更のない子ルーターは両方に同じものを入れる形が保守しやすい選択です。
0.137.0より前のコードを引き継ぐなら、app.routesを直接たどる処理を先に探します。別のミドルウェアや独立したドキュメントが要る塊は、ルーターではなくサブアプリとしてmountします。
FastAPI 0.162系のAPIRouterとinclude_routerの最小実装
まずはルーターを1つ作り、アプリに取り込むまでの流れを1ファイルで確かめます。
fastapi[standard]0.162.7・Python 3.11以上の検証環境
2026年11月14日時点のPyPIで、FastAPIの最新は0.162.7(2026年11月13日公開)です。動作要件は0.162.6からPython 3.11以上に上がりました。0.162.5まではPython 3.10でも入ったため、3.10の環境でバージョンを指定せずに入れると、pipは条件に合う0.162.5を選びます。FastAPIを初めて入れる場合は、FastAPIの最短の起動手順とasync/defの使い分けを先に済ませてください。
# 仮想環境を作り、FastAPIとテスト用のpytestを入れる
python3.11 -m venv .venv
source .venv/bin/activate
pip install "fastapi[standard]==0.162.7" pytest
fastapi[standard]には開発サーバーのfastapi devと、テストで使うTestClientが依存するhttpxが含まれています。
APIRouterにデコレーターで経路を書きinclude_routerで取り込む
公式のBigger Applicationsは、APIRouterを「小さなFastAPI」と説明しています。@app.getの代わりに@router.getと書き、最後にアプリへ取り込みます。
from fastapi import APIRouter, FastAPI
router = APIRouter()
@router.get("/users/")
def read_users():
return [{"name": "alice"}, {"name": "bob"}]
app = FastAPI()
app.include_router(router) # ここで初めてアプリの経路になる
include_routerを書き忘れると、アプリには何も登録されず/users/は404を返します。/docsに経路が出てこないときは、まずこの1行を探します。
routersディレクトリにファイル分割してmain.pyへ集約する実装手順
次に、利用者と商品の2つの資源を別ファイルに分けます。ディレクトリを「種別で切るか、ドメインで切るか」の比較はFastAPIのディレクトリ構成を2通り比べたベストプラクティスで扱っているので、ここではルーターの書き方に絞ります。
appパッケージとroutersの配置と__init__.pyが要る理由
.
├── pyproject.toml
└── app
├── __init__.py
├── main.py
├── dependencies.py
└── routers
├── __init__.py
├── items.py
└── users.py
appとroutersにはそれぞれ空の__init__.pyを置き、Pythonのパッケージとして扱わせます。これが無いと、次に出てくるfrom ..dependencies import verify_api_keyのような相対インポートでエラーが出る点に注意が必要です。起動するアプリの場所はpyproject.tomlに書いておくと、fastapi devを引数なしで実行できます。
# pyproject.toml(抜粋)
[tool.fastapi]
entrypoint = "app.main:app"
users.pyとitems.pyにprefix付きのルーターを1つずつ置く
# app/dependencies.py
from typing import Annotated
from fastapi import Header, HTTPException
def verify_api_key(x_api_key: Annotated[str | None, Header()] = None):
# 検証用の固定キー。本番では環境変数やシークレット管理から読む
if x_api_key != "secret-key":
raise HTTPException(status_code=401, detail="invalid api key")
# app/routers/users.py
from fastapi import APIRouter, HTTPException
router = APIRouter(prefix="/users", tags=["users"])
USERS = {1: {"id": 1, "name": "alice"}, 2: {"id": 2, "name": "bob"}}
@router.get("/me")
def read_me():
return USERS[1]
@router.get("/{user_id}")
def read_user(user_id: int):
if user_id not in USERS:
raise HTTPException(status_code=404, detail="user not found")
return USERS[user_id]
# app/routers/items.py
from fastapi import APIRouter, Depends, HTTPException
from ..dependencies import verify_api_key
router = APIRouter(
prefix="/items",
tags=["items"],
dependencies=[Depends(verify_api_key)],
responses={404: {"description": "Not found"}},
)
ITEMS = {"plumbus": {"name": "Plumbus", "price": 3}}
@router.get("")
def list_items():
return list(ITEMS.values())
@router.get("/{item_id}")
def read_item(item_id: str):
if item_id not in ITEMS:
raise HTTPException(status_code=404, detail="item not found")
return ITEMS[item_id]
@router.get("/me")は、prefixと連結されて/users/meになります。list_itemsのパスを空文字にしているのは、/itemsのように末尾スラッシュなしで受けるためです(理由は後の章で説明します)。
main.pyでモジュールごと読み込みrouter名の上書きを防ぐ
# app/main.py
from fastapi import FastAPI
from .routers import items, users
app = FastAPI()
app.include_router(users.router)
app.include_router(items.router)
@app.get("/health")
def health():
return {"ok": True}
from .routers.users import routerとfrom .routers.items import routerを続けて書くと、2行目が1行目のrouterを上書きし、利用者のエンドポイントが消えます。エラーにならないので気づきにくい失敗です。公式も、モジュールごと読み込んでusers.routerのように呼び分ける書き方を勧めています。
# 起動して、別の端末から呼ぶ
fastapi dev
curl http://127.0.0.1:8000/users/me
# APIキーなしは verify_api_key で止まり 401 {"detail":"invalid api key"}
curl -i http://127.0.0.1:8000/items
curl -H "x-api-key: secret-key" http://127.0.0.1:8000/items
ルーターに渡せる設定は多くありません。0.162.7のrouting.pyで数えると、APIRouterの引数は18個、include_routerは10個です。実務でまず使うのは次の6つです。
| 引数 | APIRouter | include_router | 効き方 |
|---|---|---|---|
| prefix | あり | あり | 親の後ろに連結 |
| tags | あり | あり | リストを連結 |
| dependencies | あり | あり | リストを連結 |
| responses | あり | あり | 辞書を合成 |
| deprecated | あり | あり | どちらかがTrue |
| include_in_schema | あり | あり | 両方Trueで掲載 |
lifespan・route_class・redirect_slashesはAPIRouter側にしかありません。引数の一覧と型は公式リファレンスのAPIRouter classで確認できます。
prefixの末尾スラッシュ禁止と空パスで起動時に止まる2条件
APIRouter(prefix="/items/")と書くと、アプリを読み込んだ時点でAssertionError: A path prefix must not end with '/'が出て起動しません。prefixは「/」で始めて「/」で終えない、が規則です。経路の側が「/」で始まるので、末尾に付けると二重になるからです。
もう1つは、prefixの無いルーターで@router.get("")と書いた場合です。取り込む時点でFastAPIError: Prefix and path cannot be both emptyになります。空パスはprefixを付けたルーターでだけ使えます。
ルーターを入れ子にしたり、取り込む側で引数を足したりすると、設定は上書きではなく連結されます。0.162.7のソースでは、アプリ→include_routerの引数→APIRouterの引数→デコレーターの順にリストがつながります。
app = FastAPI(dependencies=[Depends(log_request)]) # 1番目
router = APIRouter(prefix="/items", tags=["items"],
dependencies=[Depends(verify_api_key)]) # 3番目
@router.get("/{item_id}", tags=["detail"],
dependencies=[Depends(check_quota)]) # 4番目
def read_item(item_id: str, user: CurrentUser): # 引数の依存は最後
...
app.include_router(router, prefix="/api",
tags=["public"],
dependencies=[Depends(check_maintenance)]) # 2番目
# 経路は /api/items/{item_id}、OpenAPIのタグは ["public", "items", "detail"]
公式のBigger Applicationsも、ルーターの依存→デコレーターの依存→引数の依存の順に実行すると書いています。メンテナンス中の503のように、他のすべての検査より先に返したい処理は外側(アプリかinclude_router)に置くのが、ここでの処理順の決め方です。Swagger UIは先頭のタグだけでなく全部のタグの下に経路を並べるため、タグを重ねすぎると同じ経路が何か所にも出ます。表示の調整はFastAPIのSwagger UIの表示・設定と本番で隠す手順を参照してください。
include_router側で依存を足して同じルーターを使い回す判断
公式の例では、共有ライブラリのadmin.pyを書き換えずに、取り込む側のinclude_router(admin.router, prefix="/admin", dependencies=[...])で依存を足しています。ルーターを書く人と使う人が別で、使う側ごとに認証方式が違うときの書き方です。
自分たちで書いて使うルーターなら、依存はAPIRouterの引数に書きます。main.pyの取り込み行に認証を書くと、items.pyだけを読んだ人が認証の有無を見落とすからです。Dependsそのものの書き方や、戻り値を使う依存との違いはFastAPIのDependsの使い方とテストでの差し替えで扱っています。
ルーターの入れ子で/api/v1と/api/v2を分けるバージョニングの実装
応答の形を変える必要が出たとき、旧版を残したまま新版を出す方法です。URLに版番号を入れる形をルーターの入れ子で組みます。
親ルーターにprefixを付けて変更のない子ルーターを使い回す
# app/main.py(v2を追加した版)
from fastapi import APIRouter, FastAPI
from .routers import items, users, users_v2
api_v1 = APIRouter(prefix="/api/v1")
api_v1.include_router(users.router)
api_v1.include_router(items.router)
api_v2 = APIRouter(prefix="/api/v2")
api_v2.include_router(users_v2.router) # 応答の形を変えたものだけ差し替える
api_v2.include_router(items.router) # 変更のないルーターは同じものを入れる
app = FastAPI()
app.include_router(api_v1, deprecated=True) # v1は仕様書で非推奨と表示する
app.include_router(api_v2)
これで/api/v1/users/meと/api/v2/users/meが並び、商品の経路は両方の版で同じ関数が動きます。deprecated=Trueはv1配下の全経路に掛かり、OpenAPIの仕様書とSwagger UIで取り消し線つきの表示になります。動きは止めないので、利用者へ移行を促す段階に向いた指定です。
同じルーターを別のprefixで2回取り込むことは、公式も「/api/v1と/api/latestの両方で公開する」例として認めています。
カスタムのoperationIdが版の間で衝突するときの直し方
既定のoperationIdは関数名・パス・メソッドから作られます(read_me_api_v1_users_me_getのような形)。パスが違えば値も違うので、2回取り込んでも衝突しません。
衝突するのは、公式のGenerating SDKsにあるように、クライアントのメソッド名を短くするためf"{route.tags[0]}-{route.name}"へ変えた場合です。v1とv2で同じitems-list_itemsが2つでき、仕様書を作るときにDuplicate Operation IDの警告が出ます。警告だけで起動は止まらないものの、SDKの生成ツールによっては片方のメソッドが上書きされて消える結果に。取り込む側で版ごとの関数を渡して分けます。
from fastapi.routing import APIRoute
def v2_unique_id(route: APIRoute) -> str:
return f"v2-{route.tags[0]}-{route.name}"
app.include_router(api_v2, generate_unique_id_function=v2_unique_id)
ルーター自身にgenerate_unique_id_functionを指定していると、そちらが優先されて取り込む側の指定は効きません。版で分けたいなら、関数は取り込む側にだけ書きます。デコレーターにoperation_id="list"のような固定値を書いた場合も同じ警告が出るので、CIでpython -W error::UserWarning -c "from app.main import app; app.openapi()"を流し、警告を失敗として扱います。
業務システムのAPIでヘッダーによる版の切り替えを今は採らない理由
0.137.0でAPIRouterにmatches()とhandle()が加わり、X-API-Versionのようなヘッダーで一致を判断するルーターも書けるようになりました。ただしリリースノートはこれを未文書化のアルファ機能とし、今後変わりうると明記しています。業務システムのAPIでは採りません。URLの版ならアクセスログを見るだけで呼ばれた版が分かり、CDNのキャッシュキーの設定漏れで別の版が返る事故も起きません。
0.137.0で変わったinclude_routerの内部構造と移行時に壊れる箇所
2026年6月14日公開の0.137.0で、ルーターの取り込み方の中身が入れ替わりました。変更のプルリクエスト(#15745)は「Breaking Changes」に分類されています。
ルートの複製廃止で直ったinclude後の子ルーターへの経路追加漏れ
0.136以前のinclude_routerは、取り込む瞬間に子ルーターの経路を1本ずつ複製して親へ移していました。そのため、include_routerより後に子ルーターへ足した経路は親に届かず、404になりました。モジュールの読み込み順によって経路が消える、原因の追いにくい不具合です。
0.137.0からは経路を複製せず、「XはYを含み、YはZを含む」という関係ごと保持します。取り込んだ後に足した経路も反映され、子ルーターを経路より先に取り込む順序も可能です。日本語版のチュートリアルにも同じ説明が入っています。
app.routesを直接たどる処理を版上げ前に探して置き換える
壊れるのは、app.routesやrouter.routesを平らなリストとして扱っていたコードです。0.137.0以降は中間のオブジェクトが混ざる木構造になり、リリースノートは「内部の実装の詳細と見なすこと」と書いています。権限表の自動生成や「全経路に認証が掛かっているか」を確かめるテストでよく使われていた書き方です。
# 0.136以前に多かった書き方。0.137.0以降は入れ子の中身が平らに並ばない
for route in app.routes:
print(route.path)
# 公開されている経路の一覧が欲しいだけなら、OpenAPIの仕様書から取る
for path, operations in app.openapi()["paths"].items():
print(path, sorted(operations))
版を上げる前に、リポジトリ全体で.routesを含む行を洗い出します。0.137.2でfastapi.routing.iter_route_contexts()も足されましたが、上級者向けの位置づけです。仕様書に載る範囲で足りるなら、app.openapi()の方が版の変更に強い書き方といえます。
ルート順序と末尾スラッシュで分割後に422や307が返る原因と直し方
分割したあとに出る不具合の多くは、経路の照合の仕組みから来ます。
/users/meが422になる定義順の問題とルーター内の並べ方
FastAPIは経路を定義した順に照合し、最初に一致したものを使います。users.pyで/{user_id}を/meより先に書くと、/users/meはuser_id="me"として受け取られます。intに変換できないので、返るのは422です。
公式のPath Parametersも「Order matters」の節で、固定のパスを先に書くよう求めています。ルーターをまたいでも同じ規則で、include_routerを書いた順が照合の順です。同じprefixを2つのルーターで共有するのは避け、1つのprefixは1つのルーターに持たせます。
一覧の経路で末尾スラッシュの有無による307が返る仕組みとパスの統一
サンプルのlist_itemsを@router.get("/")で書くと、経路は/items/になります。ここへ/itemsで来たリクエストは、一致しないまま「末尾にスラッシュを足せば一致するか」を試され、一致すれば307のリダイレクトが返ります。APIRouterとFastAPIのredirect_slashesが既定でTrueだからです。
ブラウザは307を追いかけるので動きはしても、1往復増えます。CORSの事前確認を伴う呼び出しは、リダイレクトで失敗することもあります。フロントエンドが末尾スラッシュなしで呼ぶなら、一覧の経路は空文字で定義して/itemsに合わせ、redirect_slashesには頼りません。
APIRouterで分けるべき単位とmountやサービス層に回す場面の判断基準
最後に、ルーターで分けるもの、分けないものの線を引きます。
URLの資源単位で1ファイル1ルーターにする基準と分けすぎの失敗
ルーターの単位は、URLの最初の区切り(/users、/items、/orders)と揃えます。prefix・タグ・認証の要否が資源ごとに決まるので、ルーターの引数にそのまま書けます。公式によれば、ルーターの取り込みは性能に影響しない設計で、ファイルが増えても応答は遅くなりません。
失敗しやすいのは、HTTPメソッドごとや画面ごとにルーターを分けることです。users_get.pyとusers_post.pyのように分けると、同じprefixを2つのルーターが持ち、先の章の順序の問題を自分で作ります。1ファイルが長くなったら、ルーターを分けるのではなく、処理をサービス層の関数に移してルーターを薄くします。
APIRouterで分けず、mountやサービス層に回す場面
ルーターは「ミニFastAPI」ですが、アプリから切り離されてはいません。0.137.0のリリースノートでも、ルーター単位の依存の扱いの拡張、例外ハンドラー、ミドルウェアは「今後可能になる機能」と書かれています。ある塊にだけ別のCORS設定や別のミドルウェア、独立したSwagger UIが要るなら、ルーターではなく別のFastAPI()を作ってapp.mount("/partner", partner_app)で載せるのが正解です。公式のSub Applicationsのとおり、サブアプリは/partner/docsに自分だけのドキュメントを持ちます。
もう一方の境界はサービス層です。在庫の引き当てや請求の計算のような業務の判定は、ルーターの関数に書かずに普通の関数へ置きます。ルーターの関数は「リクエストを受け取り、サービスを呼び、応答の形に整える」ところまでが担当です。こうしておけば、HTTPを通らないバッチ処理からも同じ判定を呼べます。既存のAPIをルーター単位に分け直す設計や、版を分けた移行計画から相談したい場合は、API開発・システム連携の受託開発で受け付けています。
よくある質問
FastAPIのAPIRouterについて、実装の途中で出てくることの多い質問に、2026年11月時点の版で答えます。
APIRouterとFlaskのBlueprintは何が違いますか?
役割は同じで、公式もBlueprintに相当すると説明しています。違いは、FastAPIではルーターのprefix・タグ・依存がそのままOpenAPIの仕様書に反映される点です。Blueprintのbefore_requestで書いていた検査は、ルーターのdependenciesへ移すのが素直な置き換えです。同じAPIを両方で書いた比較はFlaskとFastAPIの違いを同じAPIで比べた記事にまとめています。
APIRouterにlifespanを書くとアプリの起動時に動きますか?
動きます。APIRouter(lifespan=...)で渡した処理は、include_routerで取り込むとアプリのlifespanとまとめて実行されます。入れ子のルーターのlifespanをまとめる扱いは0.112.2で修正されました。DB接続プールやモデルの読み込みをどこに置くかは、FastAPIのlifespanの使い方とon_eventからの移行で扱っています。
ルーターごとに例外ハンドラーを分けられますか?
0.162.7の時点では、公式の機能として用意されていません。exception_handlerはアプリに登録し、例外クラスを資源ごとに分けて振り分けます。応答の形がまったく違う塊は、サブアプリにしてmountすればそのアプリだけのハンドラーを持てます。
ルーターの依存は404のときにも実行されますか?
実行されません。依存はどれかの経路に一致してから解決されるため、存在しないパスへのリクエストではルーターのdependenciesは動きません。0.137.0のリリースノートでも、404で依存を動かす案は検討の末に見送ったと書かれています。全リクエストに掛けたい計測やログはミドルウェアに置きます。
ルーターだけを単体でテストできますか?
できます。テストの中でFastAPI()を新しく作り、対象のルーターだけをinclude_routerしてからTestClientに渡します。他のルーターの初期化やDB接続を待たずに試せるので、テストの実行時間を短くできる構成です。認証の依存はapp.dependency_overridesで差し替えます。差し替えの書き方と戻し忘れの注意点はDependsの解説記事のテストの章を参照してください。
関連記事
- FastAPIとは?最短の起動手順とasync/defの使い分け、0.162の変更点:FastAPI単体の入門と起動手順です。
- FastAPIのベストプラクティス|ディレクトリ構成・SQLAlchemy 2.1の設計・テストを実測で解説:ルーターを置くディレクトリの切り方とDB設計です。
- FastAPIのDependsの使い方|依存性注入・yield・テストの差し替えをコードで解説:ルーターのdependenciesに渡す依存関数の書き方です。
- FastAPIのlifespanの使い方|on_eventからの移行とDB接続・モデル読込をコードで解説:ルーターとアプリの起動・終了処理です。
- FastAPIのSwagger UI(/docs)の使い方|表示・設定・本番で隠す手順【2026年版】:タグごとの表示と本番での隠し方です。