開発

FastAPIのrouterの使い方|APIRouterでのファイル分割・prefix・依存・バージョニングをコードで解説

FastAPIのrouterの使い方|APIRouterでのファイル分割・prefix・依存・バージョニングをコードで解説

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

ルーター単位のprefix・tags・dependencies・responses指定

ルーターに渡せる設定は多くありません。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を付けたルーターでだけ使えます。

tagsとdependenciesが親から子へ連結される順序と実行順

ルーターを入れ子にしたり、取り込む側で引数を足したりすると、設定は上書きではなく連結されます。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の解説記事のテストの章を参照してください。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.11.13 テックブログ 宝くじ公式サイト不正アクセス:4,436名の対象確認とパスワード再設定、変更された仕組み
  2. 2026.11.13 テックブログ KARTEの不正アクセス:全機能停止で利用企業がタグ・配信・データ連携を確かめる手順
  3. 2026.11.14 テックブログ ヤマダデンキの不正アクセス:リフォーム管理システム最大350万件と、住所・見積を持つ営業記録の守り方
  4. 2026.10.21 テックブログ マネックス証券の不正アクセス:メールアドレス最大約113万件と本人確認画像1,592件の漏えいのおそれ、口座開設停止とPHP脆弱性との関係
  5. 2026.11.11 テックブログ Microsoft 2026年11月の月例更新:Outlookの悪用済みCVE-2026-124194と修正版の確認方法

RELATED POSTS 関連記事

目次