---
title: "FastAPIのrouterの使い方｜APIRouterでのファイル分割・prefix・依存・バージョニングをコードで解説"
url: "https://www.issoh.co.jp/tech/details/19741/"
published: 2026-11-14
updated: 2026-11-14
categories: ["開発"]
publisher: "株式会社一創"
---

# 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](https://pypi.org/project/fastapi/)（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の使い分け](https://www.issoh.co.jp/tech/details/3859/)を先に済ませてください。

```
# 仮想環境を作り、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](https://fastapi.tiangolo.com/tutorial/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通り比べたベストプラクティス](https://www.issoh.co.jp/tech/details/4347/)で扱っているので、ここではルーターの書き方に絞ります。

### 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](https://github.com/fastapi/fastapi/blob/0.162.7/fastapi/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](https://fastapi.tiangolo.com/reference/apirouter/)で確認できます。

### 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の表示・設定と本番で隠す手順](https://www.issoh.co.jp/tech/details/2246/)を参照してください。

### include\_router側で依存を足して同じルーターを使い回す判断

公式の例では、共有ライブラリの`admin.py`を書き換えずに、取り込む側の`include_router(admin.router, prefix="/admin", dependencies=[...])`で依存を足しています。ルーターを書く人と使う人が別で、使う側ごとに認証方式が違うときの書き方です。

自分たちで書いて使うルーターなら、依存は`APIRouter`の引数に書きます。`main.py`の取り込み行に認証を書くと、`items.py`だけを読んだ人が認証の有無を見落とすからです。`Depends`そのものの書き方や、戻り値を使う依存との違いは[FastAPIのDependsの使い方とテストでの差し替え](https://www.issoh.co.jp/tech/details/19720/)で扱っています。

## ルーターの入れ子で/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](https://fastapi.tiangolo.com/advanced/generate-clients/)にあるように、クライアントのメソッド名を短くするため`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`のようなヘッダーで一致を判断するルーターも書けるようになりました。ただし[リリースノート](https://fastapi.tiangolo.com/release-notes/)はこれを未文書化のアルファ機能とし、今後変わりうると明記しています。業務システムのAPIでは採りません。URLの版ならアクセスログを見るだけで呼ばれた版が分かり、CDNのキャッシュキーの設定漏れで別の版が返る事故も起きません。

## 0.137.0で変わったinclude\_routerの内部構造と移行時に壊れる箇所

2026年6月14日公開の0.137.0で、ルーターの取り込み方の中身が入れ替わりました。[変更のプルリクエスト（#15745）](https://github.com/fastapi/fastapi/pull/15745)は「Breaking Changes」に分類されています。

### ルートの複製廃止で直ったinclude後の子ルーターへの経路追加漏れ

0.136以前の`include_router`は、取り込む瞬間に子ルーターの経路を1本ずつ複製して親へ移していました。そのため、`include_router`より後に子ルーターへ足した経路は親に届かず、404になりました。モジュールの読み込み順によって経路が消える、原因の追いにくい不具合です。

0.137.0からは経路を複製せず、「XはYを含み、YはZを含む」という関係ごと保持します。取り込んだ後に足した経路も反映され、子ルーターを経路より先に取り込む順序も可能です。[日本語版のチュートリアル](https://fastapi.tiangolo.com/ja/tutorial/bigger-applications/)にも同じ説明が入っています。

### 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](https://fastapi.tiangolo.com/tutorial/path-params/)も「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](https://fastapi.tiangolo.com/advanced/sub-applications/)のとおり、サブアプリは`/partner/docs`に自分だけのドキュメントを持ちます。

もう一方の境界はサービス層です。在庫の引き当てや請求の計算のような業務の判定は、ルーターの関数に書かずに普通の関数へ置きます。ルーターの関数は「リクエストを受け取り、サービスを呼び、応答の形に整える」ところまでが担当です。こうしておけば、HTTPを通らないバッチ処理からも同じ判定を呼べます。既存のAPIをルーター単位に分け直す設計や、版を分けた移行計画から相談したい場合は、[API開発・システム連携の受託開発](https://www.issoh.co.jp/service/system/api/)で受け付けています。

## よくある質問

FastAPIの`APIRouter`について、実装の途中で出てくることの多い質問に、2026年11月時点の版で答えます。

### APIRouterとFlaskのBlueprintは何が違いますか？

役割は同じで、公式もBlueprintに相当すると説明しています。違いは、FastAPIではルーターのprefix・タグ・依存がそのままOpenAPIの仕様書に反映される点です。Blueprintの`before_request`で書いていた検査は、ルーターの`dependencies`へ移すのが素直な置き換えです。同じAPIを両方で書いた比較は[FlaskとFastAPIの違いを同じAPIで比べた記事](https://www.issoh.co.jp/tech/details/19695/)にまとめています。

### APIRouterにlifespanを書くとアプリの起動時に動きますか？

動きます。`APIRouter(lifespan=...)`で渡した処理は、`include_router`で取り込むとアプリのlifespanとまとめて実行されます。入れ子のルーターのlifespanをまとめる扱いは0.112.2で修正されました。DB接続プールやモデルの読み込みをどこに置くかは、[FastAPIのlifespanの使い方とon\_eventからの移行](https://www.issoh.co.jp/tech/details/19739/)で扱っています。

### ルーターごとに例外ハンドラーを分けられますか？

0.162.7の時点では、公式の機能として用意されていません。`exception_handler`はアプリに登録し、例外クラスを資源ごとに分けて振り分けます。応答の形がまったく違う塊は、サブアプリにして`mount`すればそのアプリだけのハンドラーを持てます。

### ルーターの依存は404のときにも実行されますか？

実行されません。依存はどれかの経路に一致してから解決されるため、存在しないパスへのリクエストではルーターの`dependencies`は動きません。0.137.0のリリースノートでも、404で依存を動かす案は検討の末に見送ったと書かれています。全リクエストに掛けたい計測やログはミドルウェアに置きます。

### ルーターだけを単体でテストできますか？

できます。テストの中で`FastAPI()`を新しく作り、対象のルーターだけを`include_router`してから`TestClient`に渡します。他のルーターの初期化やDB接続を待たずに試せるので、テストの実行時間を短くできる構成です。認証の依存は`app.dependency_overrides`で差し替えます。差し替えの書き方と戻し忘れの注意点は[Dependsの解説記事のテストの章](https://www.issoh.co.jp/tech/details/19720/)を参照してください。

## 関連記事

- [FastAPIとは？最短の起動手順とasync/defの使い分け、0.162の変更点](https://www.issoh.co.jp/tech/details/3859/)：FastAPI単体の入門と起動手順です。
- [FastAPIのベストプラクティス｜ディレクトリ構成・SQLAlchemy 2.1の設計・テストを実測で解説](https://www.issoh.co.jp/tech/details/4347/)：ルーターを置くディレクトリの切り方とDB設計です。
- [FastAPIのDependsの使い方｜依存性注入・yield・テストの差し替えをコードで解説](https://www.issoh.co.jp/tech/details/19720/)：ルーターのdependenciesに渡す依存関数の書き方です。
- [FastAPIのlifespanの使い方｜on\_eventからの移行とDB接続・モデル読込をコードで解説](https://www.issoh.co.jp/tech/details/19739/)：ルーターとアプリの起動・終了処理です。
- [FastAPIのSwagger UI（/docs）の使い方｜表示・設定・本番で隠す手順【2026年版】](https://www.issoh.co.jp/tech/details/2246/)：タグごとの表示と本番での隠し方です。

---

出典: [FastAPIのrouterの使い方｜APIRouterでのファイル分割・prefix・依存・バージョニングをコードで解説](<https://www.issoh.co.jp/tech/details/19741/>)（株式会社一創）
