# FastAPI 大規模アプリ設計：APIRouter・依存性注入の階層化・プロジェクト構成で『保守できるAPI』を作る

> FastAPIで大規模APIを保守可能に保つ設計ガイド。公式 Bigger Applications に忠実な APIRouter・推奨プロジェクト構成・相対インポートと、グローバル/ルーター/デコレータ/引数の4階層DIに加え、router→service→repositoryのレイヤリング・循環インポート回避・APIバージョニング・docs整備・テストまで実コードで解説します。

- 公開日: 2026-06-26
- 著者: 友田 陽大
- タグ: Python, FastAPI, アーキテクチャ設計, 依存性注入, 保守性
- URL: https://tomodahinata.com/blog/fastapi-project-structure-apirouter-dependencies-large-app-guide

## 要点

- 単一main.pyは『認証＋複数リソース＋DB』が揃った瞬間に破綻する。機能ごとにAPIRouterへ切り出し、公式推奨のapp/（main.py・dependencies.py・routers/・internal/）へ分割する
- include_routerはprefix/tags/dependencies/responsesを付与でき、元のrouterは変更されない（同じrouterを別構成で再利用できる）。router.include_routerでネストも可能
- 依存性注入は4階層で設計する：全体=FastAPI(dependencies=)、機能群=APIRouter(dependencies=)、副作用のみ=デコレータのdependencies=[]、戻り値を使う=関数引数のDepends
- routerはHTTP境界だけに薄く保ち、ビジネスロジックはservice、I/Oはrepositoryへ寄せる（SRP/ETC）。依存方向を一方向に固定すれば循環インポートは構造的に起きない
- 本番の仕上げ：APIバージョニング（/api/v1）、openapi_tagsでdocsを資産化しつつ本番はdocs_url=Noneで露出を絞る、create_appファクトリでテスト隔離、fastapi dev / fastapi run で実行

---

最初の `main.py` は美しい。`@app.get("/")` が並び、5分で動く。けれど**認証が入り、リソースが3つに増え、DB が刺さった瞬間**、その `main.py` は 2,000 行のボール・オブ・マッド（泥団子）になります。エンドポイントを足すたびに既存が壊れ、テストは遅く、新しいメンバーはどこに何を書けばいいか分からない——**「動く」と「保守できる」は別物**です。

この記事は、FastAPI で**大規模・本番品質のアプリ構造**を作るためのガイドです。FastAPI 公式の「Bigger Applications」が示す `APIRouter` とプロジェクト構成を**最新仕様に忠実**に追いつつ、公式チュートリアルが（教材ゆえに）触れない領域——**いつ分割すべきか（YAGNI）、router/service/repository のレイヤリング、循環インポートの構造的回避、API バージョニング、docs のハードニング、テスト容易性**——まで踏み込みます。題材として、私が国内大手放送事業者向けに構築した社内AIプラットフォーム（[複数のFastAPIサービスを単一の規約で束ねたモノレポ](/case-studies/broadcaster-ai-content-platform)。共通の認証・ロギング・キャッシュ規約を1つに統一し、横展開可能なプラットフォームとして設計）での判断も交えます。

> **この記事のルール**：構成・API は **FastAPI 公式ドキュメント（2026年6月時点）** に基づきます。FastAPI は近年、開発・本番の起動に **`fastapi` CLI（`fastapi dev` / `fastapi run`）** と `pyproject.toml` の `[tool.fastapi]` エントリポイント設定を備えました。本記事はこの最新版に準拠します。仕様は改定されるため、本番投入前に必ず公式で最新の挙動を確認してください。設計の前提として、**非同期の使い分け・lifespan・可観測性は[本番運用ガイド](/blog/fastapi-production-async-pydantic-observability-guide)、認証・認可は[認証ガイド](/blog/fastapi-authentication-oauth2-jwt-security-scopes-production-guide)**を併読してください。本記事は「それらを**どう配置して保守するか**」に集中します。

---

## 0. まず判断：いつ分割するのか（YAGNI）

構造化の前に、**「まだ分割しない」判断**を持つことが大事です。3エンドポイントのツールに `services/` `repositories/` を切るのは過剰設計（YAGNI 違反）で、かえって読みにくくなります。

分割の**サイン**を基準にします。

- `main.py` が**1画面（〜200行）を超えた**。
- **リソース（名詞）が3つ以上**になった（users / items / orders …）。
- **横断的関心事**（認証・DBセッション・ロギング）が複数ハンドラに**コピペ**され始めた。
- **チームが2人以上**になり、同じファイルでコンフリクトし始めた。

逆に、上のどれにも当てはまらないなら、**1ファイルのままが正解**（KISS）。本記事は「分割すべき段階に来たアプリ」を対象にします。

---

## 1. `APIRouter`：機能ごとにルートを切り出す

分割の最小単位が **`APIRouter`** です。公式の言葉どおり——**`APIRouter` は `FastAPI` クラスと全く同じように動きます**。パラメータ・レスポンス・依存・タグなど、同じオプションがすべて使えます。「ミニ FastAPI」だと思えば正確です。

```python
# app/routers/users.py
from fastapi import APIRouter

router = APIRouter()    # この機能（users）のルートを束ねる入れ物

@router.get("/users/", tags=["users"])
async def read_users():
    return [{"username": "Rick"}, {"username": "Morty"}]

@router.get("/users/{username}", tags=["users"])
async def read_user(username: str):
    return {"username": username}
```

`@app.get` を `@router.get` に変えるだけ。これで `users` に関するルートは**1ファイルに凝集**し、`main.py` から消えます（SRP：このファイルが変わる理由は「ユーザー機能の変更」だけ）。

---

## 2. 公式推奨のプロジェクト構成

公式が示す標準レイアウトはこれです。**まずこの形を正しく作る**ことが、すべての土台になります。

```text
.
├── app                  # "app" は Python パッケージ
│   ├── __init__.py      # これで "app" がパッケージになる
│   ├── main.py          # アプリ本体（FastAPI() と include_router）
│   ├── dependencies.py  # 共有の依存（認証・共通ヘッダ検証など）
│   ├── routers          # "routers" はサブパッケージ
│   │   ├── __init__.py
│   │   ├── items.py     # items 機能のルート
│   │   └── users.py     # users 機能のルート
│   └── internal         # 社内/管理用など外に出さない機能
│       ├── __init__.py
│       └── admin.py
```

ポイントは3つです。

### 2.1 `__init__.py` は「省略可能な飾り」ではない

各ディレクトリの **`__init__.py` が、そのディレクトリを Python パッケージにします**。これがあるから `from app.routers import items` のようなインポートが成立します。空ファイルで構いませんが、**消すとインポートが壊れます**。

### 2.2 共有依存は `dependencies.py` に集約する（DRY）

認証チェックや共通ヘッダ検証など、**複数の機能から使われる依存**は1箇所にまとめます。

```python
# app/dependencies.py
from typing import Annotated
from fastapi import Header, HTTPException

async def get_token_header(x_token: Annotated[str, Header()]):
    if x_token != "fake-super-secret-token":
        raise HTTPException(status_code=400, detail="X-Token header invalid")

async def get_query_token(token: str):
    if token != "jessica":
        raise HTTPException(status_code=400, detail="No Jessica token provided")
```

### 2.3 相対インポートで参照する

`routers/items.py` から `dependencies.py` を参照するときは**相対インポート**を使います。

```python
from ..dependencies import get_token_header
#   ^^  二つのドット = 親パッケージ（app）まで上がってから dependencies を見る
#   一つのドット（.）= 同じパッケージ内
```

> **名前衝突を避ける定石**：`from .routers.items import router` と `from .routers.users import router` を両方書くと、**後者が前者を上書き**してしまいます。公式どおり **モジュールごと import** してください——`from .routers import items, users` とし、使うときは `items.router` / `users.router` と**名前空間付き**で参照します。

---

## 3. `include_router`：機能を本体に組み込む

切り出した router を `main.py` で組み込みます。ここで **prefix・tags・dependencies・responses をまとめて付与**できるのが強力です。

```python
# app/main.py
from fastapi import Depends, FastAPI
from .dependencies import get_query_token, get_token_header
from .internal import admin
from .routers import items, users

app = FastAPI(dependencies=[Depends(get_query_token)])   # ← 全体に効く依存（第4章）

app.include_router(users.router)
app.include_router(items.router)

# admin は「組み込み時に」prefix・タグ・追加の依存・追加レスポンスを上書き付与する
app.include_router(
    admin.router,
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(get_token_header)],     # /admin 配下すべてにトークン検証を強制
    responses={418: {"description": "I'm a teapot"}},
)

@app.get("/")
async def root():
    return {"message": "Hello Bigger Applications!"}
```

`prefix` / `tags` / `dependencies` / `responses` は **router を作るとき（`APIRouter(...)`）にも、組み込むとき（`include_router(...)`）にも**指定できます。「その機能に固有の設定」は router 側、「組み込み先の事情で足す設定」は include 側、と使い分けます。

> **公式の重要仕様：元の router は変更されない**。`include_router` で prefix や依存を足しても、**元の `APIRouter` オブジェクトは無改変**のままです。だから**同じ router を、別のアプリで別の prefix・別の依存で再利用**できます。共有ライブラリ的に router を配るときに効く性質です。`prefix` は**末尾スラッシュを付けない**（`/items` であって `/items/` ではない）点だけ注意。

### 3.1 router を router に入れ子にする

router は**別の router に組み込む**こともできます。これが後述のバージョニング（`/api/v1` の束ね）の鍵になります。

```python
parent.include_router(child_router)   # APIRouter を APIRouter に入れ子にできる
```

---

## 4. 依存性注入を「4つの階層」で設計する

FastAPI の真価は**依存性注入（DI）**にあります。が、現場で迷うのは機能ではなく「**この依存をどの高さに置くか**」です。FastAPI は**4つの階層**を提供します。これを地図にすると設計が一気に楽になります。

| 階層 | 書き方 | 効く範囲 | 向く用途 |
| --- | --- | --- | --- |
| ① グローバル | `FastAPI(dependencies=[...])` | **全エンドポイント** | 全 API 共通の前提（APIキー検証・共通ヘッダ必須化） |
| ② ルーター | `APIRouter(dependencies=[...])` / `include_router(dependencies=[...])` | **その機能群すべて** | `/admin` 配下に管理者権限を強制、テナント解決 |
| ③ デコレータ | `@router.get(..., dependencies=[...])` | **その1エンドポイント** | 戻り値不要の検証・副作用（レート制限・特定権限チェック） |
| ④ 関数引数 | `param: Annotated[X, Depends(...)]` | **その1エンドポイント** | **戻り値を使う**（DBセッション・現在のユーザー） |

判断は一行です——**「戻り値をハンドラ本体で使うか？」**。使うなら④（関数引数）。使わない（検証・副作用だけ）なら①〜③のどれか。そして**効かせたい範囲が広いほど上の階層**へ置きます。

### 4.1 ① グローバル依存：全エンドポイント共通の前提

```python
# 全リクエストに先立ってヘッダ検証を走らせる（一箇所で全体に効く）
app = FastAPI(dependencies=[Depends(verify_token), Depends(verify_key)])
```

`FastAPI(dependencies=[...])` に渡した依存は、**アプリの全パスオペレーションで実行**されます。「全 API で必須の前提条件」をハンドラに書き散らさず、ここで一度だけ宣言します（DRY）。

### 4.2 ③ デコレータ依存：戻り値は要らないが、必ず実行したい

「検証はしたいが、その結果はハンドラで使わない」ときは、**デコレータの `dependencies=[...]`** が最適です。

```python
@app.get("/items/", dependencies=[Depends(verify_token), Depends(verify_key)])
async def read_items():
    # verify_token / verify_key は必ず実行される（失敗すれば例外で弾かれる）。
    # ただし戻り値はハンドラに渡らない（使わないので引数にも現れない）。
    return [{"item": "Foo"}, {"item": "Bar"}]
```

これらの依存は**通常の依存と同じく例外を投げられ**（`HTTPException` で 4xx を返せる）、ヘッダ等も読めます。違いは**戻り値が無視される**こと。関数引数に「使わない依存」をぶら下げると、エディタが「未使用引数」を警告して読み手を混乱させます。**副作用だけなら、引数ではなくデコレータへ**——これは可読性の小さな、しかし効く工夫です。

### 4.3 ② ルーター依存：認可をルーター層に一元化する（最重要）

実務で最も効くのが**ルーター層の依存**です。`/admin` 配下の**全エンドポイント**に権限チェックを強制したいとき、各ハンドラに書くのは抜け漏れの温床。router に一度だけ付ければ、**書き忘れが構造的に起きません**。

```python
from fastapi import APIRouter, Security
from app.core.security import get_current_active_user   # 認証ガイド参照

# /admin 配下の全ルートに admin スコープを強制（DRY・抜け漏れ防止）
admin_router = APIRouter(
    prefix="/admin",
    tags=["admin"],
    dependencies=[Security(get_current_active_user, scopes=["admin"])],
)

@admin_router.get("/reports")     # ← ここに認可を書かなくても admin が要求される
async def list_reports():
    ...
```

放送事業者向けプラットフォームでも、**認証・ロギング・レート制限といった横断的関心事を「各サービス共通の規約」として一箇所に定義**し、5つの FastAPI サービスへ横展開しました。木材流通 B2B SaaS（[経産大臣賞受賞](/case-studies/lumber-industry-dx)）では、業種ベースの認可を**ルーター層に一元化**し、全 221 エンドポイントを一貫したポリシーで保護——第三者ペネトレで**認証欠落0件**を実証しています。認可は「各ハンドラの善意の if 文」ではなく、**境界に一元化された構造**で守るのが本番品質です（→[認証・認可ガイド](/blog/fastapi-authentication-oauth2-jwt-security-scopes-production-guide)）。

---

## 5. レイヤリング：router を薄く、ロジックは service / repository へ

ここからが公式チュートリアルの**その先**です。公式の例は学習のため、ビジネスロジックを**ハンドラ（router）の中**に書きます。これは小規模なら正解ですが、大規模では破綻します——**ハンドラが HTTP・ビジネスルール・DB アクセスを全部抱える**と、テストが重く、変更が波及します（SRP 違反）。

そこで責務を3層に分けます。

- **router（HTTP境界）**：リクエスト/レスポンスの変換だけ。**薄く保つ**。
- **service（ビジネスロジック）**：業務ルール。**HTTP も DB ドライバも知らない**。
- **repository（I/O）**：DB・外部 API への入出力だけ。

```python
# app/repositories/user_repository.py —— I/O のみ。SQLの詳細はここに閉じる
from sqlalchemy.ext.asyncio import AsyncSession
from app.models import User

class UserRepository:
    def __init__(self, db: AsyncSession) -> None:
        self._db = db

    async def get(self, user_id: int) -> User | None:
        return await self._db.get(User, user_id)

    async def add(self, user: User) -> User:
        self._db.add(user)
        await self._db.flush()
        return user
```

```python
# app/services/user_service.py —— 業務ルールのみ。HTTPもSQLも登場しない（純粋でテストしやすい）
from app.repositories.user_repository import UserRepository
from app.schemas.user import UserCreate, UserPublic

class UserService:
    def __init__(self, repo: UserRepository) -> None:
        self._repo = repo

    async def register(self, payload: UserCreate) -> UserPublic:
        # 例：業務ルール（重複チェック・初期ロール付与など）はここに集約する
        user = await self._repo.add(payload.to_model())
        return UserPublic.model_validate(user)
```

```python
# app/api/deps.py —— 「配線（DI）」を一箇所に集約する
from typing import Annotated
from fastapi import Depends
from app.core.db import DbDep                       # lifespan で作った session を注入（本番運用ガイド参照）
from app.repositories.user_repository import UserRepository
from app.services.user_service import UserService

def get_user_service(db: DbDep) -> UserService:
    # repository を生成して service に注入する。これがDIの結節点。
    return UserService(UserRepository(db))

UserServiceDep = Annotated[UserService, Depends(get_user_service)]
```

```python
# app/api/v1/routers/users.py —— router は薄い。HTTP境界の翻訳だけ
from fastapi import APIRouter, status
from app.api.deps import UserServiceDep
from app.schemas.user import UserCreate, UserPublic

router = APIRouter()

@router.post("/", response_model=UserPublic, status_code=status.HTTP_201_CREATED)
async def create_user(payload: UserCreate, service: UserServiceDep) -> UserPublic:
    # router の仕事は「受け取って、serviceに渡して、返す」だけ。業務ルールはここに無い。
    return await service.register(payload)
```

この分割の**見返り**は具体的です。

- **テスト容易性**：`UserService` は HTTP を立てずに**素のユニットテスト**で検証できる。`repository` をフェイクに差し替えれば DB も要らない。
- **拡張性（ETC）**：DB を PostgreSQL から別物へ替えても、変更は `repository` に閉じる。service・router は無傷。
- **型安全**：層の境界を Pydantic スキーマ（`UserCreate` / `UserPublic`）で固定し、入力と出力を別型にする（[境界バリデーションは Pydantic ガイド](/blog/pydantic-v2-production-validation-type-safety)）。

> **やり過ぎない（YAGNI）**：全機能に機械的に3層を切る必要はありません。**ロジックの薄い CRUD は router 直書きでも可**。「業務ルールが育ってきた機能」だけ service を生やす——増分主義が正解です。

### 5.1 大規模向けの発展レイアウト

3層を導入すると、ディレクトリは公式の最小形から次のように育ちます（**必要になってから**この形へ）。

```text
app/
├── main.py            # create_app・include_router・lifespan・middleware（第8章）
├── core/              # 全層から参照される土台（循環の起点にしない）
│   ├── config.py      # Settings（pydantic-settings）
│   ├── db.py          # engine・session factory・get_db（DbDep）
│   └── security.py    # 認証の依存（get_current_user 等）
├── api/
│   ├── deps.py        # DIの結節点（service生成・型エイリアス）
│   └── v1/
│       ├── router.py  # v1の全routerを束ねる
│       └── routers/   # users.py・items.py …（HTTP境界）
├── services/          # ビジネスロジック
├── repositories/      # I/O
├── models/            # ORMモデル
└── schemas/           # Pydanticスキーマ（入力/出力）
```

---

## 6. 循環インポートを「構造」で防ぐ

FastAPI の大規模化で**最も多い事故が循環インポート**（`ImportError: cannot import name ...`）です。原因はほぼ常に「**依存の向きが双方向になっている**」こと。これは規律で根絶できます。

**鉄則：依存は一方向にだけ流す。**

```text
main → api(routers) → services → repositories → models / schemas
                  ↘ core(config / db / security) ← 誰からでも参照してよい土台
```

- **上位は下位を import してよい。下位は上位を import してはいけない。** `repository` が `router` を import した時点で循環の芽です。
- **`core/`（設定・DB・セキュリティ）は最下層の土台**として、誰からでも import される側に置く。`core` は上位を import しない。
- **`main.py` を他モジュールから import しない。** router は `app` を必要としません（`APIRouter` を使う）。「router が `app` を欲しがる」設計になったら、それは依存の向きが逆——`include_router` で**組み込む側（main）から差し込む**のが正しい向きです。
- **型ヒントのためだけの相互参照**は、実行時 import を避けて解消します。

```python
from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:           # 型チェック時だけ評価され、実行時には import されない
    from app.services.user_service import UserService

def handler(service: "UserService") -> None: ...
```

依存の向きさえ一方向に保てば、循環は**そもそも起きようがありません**。これは「気をつける」話ではなく「**構造で不可能にする**」話です。

---

## 7. API バージョニング：壊さずに進化させる

公開 API は**後方互換**が生命線です。`/users` のレスポンス形を変えたいが既存クライアントを壊せない——そんなときは**バージョンを並走**させます。第3.1章の「router を router に入れ子」がそのまま使えます。

```python
# app/api/v1/router.py —— v1 の全機能を1つの router に束ねる
from fastapi import APIRouter
from .routers import users, items

api_router = APIRouter()
api_router.include_router(users.router, prefix="/users", tags=["users"])
api_router.include_router(items.router, prefix="/items", tags=["items"])
```

```python
# app/main.py —— /api/v1 配下にまとめて組み込む
from app.api.v1.router import api_router as v1_router
# from app.api.v2.router import api_router as v2_router   # v2 を足しても v1 は無傷

app.include_router(v1_router, prefix="/api/v1")
# app.include_router(v2_router, prefix="/api/v2")
```

新バージョンは**別ディレクトリ（`api/v2/`）に新しい router を生やし、`/api/v2` で並走**させます。v1 のコードに触れないので、**既存クライアントは1行も壊れません**（ETC）。共通ロジックは service/repository を**両バージョンで共有**し、変わるのは「HTTP の見せ方（router/schema）」だけに留めます（DRY）。

> **代替：サブアプリのマウント**。完全に独立した別アプリ（別の docs・別のライフサイクル）にしたいなら、`app.mount("/api/v2", subapp)` でサブアプリケーションを丸ごとマウントする手もあります。ただし**多くのケースは router の入れ子で十分**（KISS）。「本当に独立したアプリか？」を自問してから mount を選んでください。

---

## 8. 設定・lifespan・例外ハンドラを一元化する（create_app ファクトリ）

`main.py` でグローバルに `app = FastAPI()` を作ると、**import した瞬間に副作用**（設定読み込み・接続）が走り、テストで差し替えにくくなります。これを**ファクトリ関数**に畳むと、組み立てが一望でき、テストごとに**新鮮なアプリ**を作れます。

```python
# app/main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.core.config import Settings, get_settings
from app.core.lifespan import lifespan          # DBプール等の確保（本番運用ガイド参照）
from app.core.errors import register_exception_handlers
from app.api.v1.router import api_router as v1_router

tags_metadata = [
    {"name": "users", "description": "ユーザーの作成・取得。**ログイン**もここ。"},
    {"name": "items", "description": "アイテム管理。"},
]

def create_app(settings: Settings | None = None) -> FastAPI:
    settings = settings or get_settings()
    app = FastAPI(
        title=settings.app_name,
        version=settings.version,
        lifespan=lifespan,
        openapi_tags=tags_metadata,
        # 本番では docs/openapi を閉じて API 構造の露出を絞る（後述）
        docs_url="/docs" if settings.docs_enabled else None,
        redoc_url=None,
        openapi_url="/openapi.json" if settings.docs_enabled else None,
    )
    app.add_middleware(
        CORSMiddleware,
        allow_origins=settings.cors_origins,     # "*" は使わない（認証ガイド参照）
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )
    register_exception_handlers(app)             # 例外→HTTPの翻訳を一箇所に
    app.include_router(v1_router, prefix="/api/v1")
    return app

app = create_app()       # ASGIサーバ（fastapi run / uvicorn）が参照するエントリポイント
```

**ファクトリの見返り**：テストでは `create_app(test_settings)` で**本番と隔離されたアプリ**を毎回新規生成できます。グローバル副作用が無いので、テスト間の汚染も起きません（テスト容易性）。

---

## 9. ドキュメントを「資産」にする（そして本番では露出を絞る）

FastAPI の自動 docs は強力な資産です。**タグの説明とメタデータ**を整えるだけで、`/docs` が読めるAPI仕様書になります。

```python
app = FastAPI(
    title="ChimichangApp",
    summary="一行サマリ（OpenAPI 3.1 / FastAPI 0.99.0+）",
    description="Markdownが使える詳細説明。**太字**やリストも可。",
    version="1.4.0",
    contact={"name": "API Support", "url": "https://example.com/contact", "email": "api@example.com"},
    license_info={"name": "Apache 2.0", "url": "https://www.apache.org/licenses/LICENSE-2.0.html"},
    openapi_tags=tags_metadata,    # ← この **リストの順序が docs の表示順**になる
)
```

`openapi_tags` の各要素は `name`（必須）・`description`（Markdown 可）・`externalDocs` を持てます。そして**リストに並べた順序が、そのまま docs UI のセクション順**になります。乱雑なアルファベット順ではなく、**読み手にとって自然な業務順**に並べられます。

> **【セキュリティ】本番では docs を無防備に晒さない**：`/docs`・`/redoc`・`/openapi.json` は**API の全構造（全エンドポイント・全スキーマ）を公開**します。社内 API や認証付き API では、これは攻撃者への地図になり得ます。本番では `docs_url=None` / `redoc_url=None` / `openapi_url=None` で**無効化**するか、**認証の背後に隠す**のが定石です（第8章のように環境で出し分け）。`openapi_url=None` にすると docs UI も同時に無効化されます。

---

## 10. 実行とテスト：`fastapi` CLI と router 単位の検証

### 10.1 起動：開発は `fastapi dev`、本番は `fastapi run`

FastAPI は現在、専用 CLI を備えています。**開発はリロード付きの `fastapi dev`、本番はリロード無しの `fastapi run`**（内部で uvicorn を駆動）。

```bash
fastapi dev app/main.py     # 開発：自動リロード（既定は 127.0.0.1:8000）
fastapi run app/main.py     # 本番：リロード無し（既定は 0.0.0.0:8000）
```

エントリポイントは `pyproject.toml` に書いておけます（`fastapi run` がアプリを自動発見）。

```toml
[tool.fastapi]
entrypoint = "app.main:app"   # from app.main import app と同義
```

本番のワーカー多重化・グレースフルシャットダウンは[本番運用ガイドの第9章](/blog/fastapi-production-async-pydantic-observability-guide)に従ってください（プロセスマネージャ＋複数ワーカー、`lifespan` での後始末）。

### 10.2 テスト：router 単位 ＋ `dependency_overrides`

構造化の最大の見返りが**テスト容易性**です。`create_app` で隔離アプリを作り、依存を差し替えます。

```python
# tests/test_users.py
import pytest
from fastapi.testclient import TestClient
from app.main import create_app
from app.api.deps import get_user_service

@pytest.fixture
def client():
    app = create_app(test_settings())             # 本番から隔離した新鮮なアプリ
    # service を「フェイク」に差し替える → DB を立てずに router の契約だけ検証できる
    app.dependency_overrides[get_user_service] = lambda: FakeUserService()
    yield TestClient(app)
    app.dependency_overrides.clear()              # テスト間の汚染を防ぐ

def test_create_user_returns_201(client):
    res = client.post("/api/v1/users/", json={"email": "a@example.com", "display_name": "A"})
    assert res.status_code == 201
    assert "id" in res.json()
```

ポイントは2つ。**(1) `service` 層は DB 無しで単体テストできる**（第5章の分割の回収）。**(2) router は `dependency_overrides` で service をフェイクに差し替え、HTTP 契約（ステータス・スキーマ）だけを高速に検証できる**。レイヤリングと DI が、そのままテスト戦略に直結します。

---

## 11. まとめ：保守できる FastAPI 構成チートシート

迷ったときの早見表です。

- **分割の判断**：`main.py` が200行超／リソース3つ以上／横断的関心事がコピペされ始めたら分割（YAGNI：それまでは1ファイルで可）。
- **APIRouter**：機能ごとに `router = APIRouter()` で切り出す。`FastAPI` クラスと同じオプションが使える「ミニ FastAPI」。
- **構成**：公式の `app/`（`main.py`・`dependencies.py`・`routers/`・`internal/`）。各 dir に `__init__.py`。相対インポート（`.` / `..`）。router は**モジュールごと import** して名前衝突を避ける。
- **include_router**：`prefix`/`tags`/`dependencies`/`responses` を付与。**元の router は無改変**で再利用可。`router.include_router` で入れ子。
- **DI 4階層**：全体=`FastAPI(dependencies=)`、機能群=`APIRouter(dependencies=)`、副作用のみ=デコレータ`dependencies=[]`、**戻り値を使う**=関数引数 `Depends`。判断は「戻り値を使うか・効かせる範囲はどこまでか」。
- **レイヤリング**：router を薄く、業務は service、I/O は repository（SRP/ETC）。ロジックが育った機能だけ service を生やす（YAGNI）。
- **循環インポート**：依存は一方向（`main→api→services→repositories→models`、`core` は土台）。型のみ参照は `TYPE_CHECKING`。
- **バージョニング**：`/api/v1` を router 入れ子で束ね、`/api/v2` を並走。共通ロジックは service で共有。完全独立なら `mount`。
- **docs**：`openapi_tags` の順序で並べる。**本番は `docs_url=None` 等で露出を絞る**。
- **実行/テスト**：`fastapi dev`（開発）/`fastapi run`（本番）、`[tool.fastapi] entrypoint`。`create_app` ファクトリ＋`dependency_overrides` で隔離テスト。

---

FastAPI は「5分で動く」フレームワークですが、**6ヶ月後も触れる**かどうかは初期の構造で決まります。**機能を router に凝集し、横断的関心事を依存の正しい高さに置き、ロジックを層に分け、依存の向きを一方向に固定する**——どれも派手ではありませんが、この積み重ねが「**エンドポイントが200本に増えても破綻しない API**」を作ります。

私は放送事業者向けの社内 AI プラットフォームで、**複数の FastAPI サービスを単一の認証・ロギング・キャッシュ規約で束ねたモノレポ**を設計し、木材流通 SaaS では**全221エンドポイントを一貫した構造で保守**して第三者ペネトレで認証欠落0件を実証しました。生成 AI（Claude Code）を相棒に、**一人で速く・安く**作りつつ、規約とテストで品質を担保するのが私の進め方です。

**「FastAPI のこの API、機能が増えて main.py が苦しい」「バージョニングやレイヤリングをどう入れるべきか」——その設計判断から実装・テスト・運用まで、一気通貫で伴走します。** 要件整理の段階からでも、お気軽にご相談ください。

---

### 参考（公式ドキュメント）

- [Bigger Applications - Multiple Files（FastAPI）](https://fastapi.tiangolo.com/tutorial/bigger-applications/) — `APIRouter`・プロジェクト構成・`include_router`・相対インポート
- [Global Dependencies（FastAPI）](https://fastapi.tiangolo.com/tutorial/dependencies/global-dependencies/) — `FastAPI(dependencies=[...])` で全体に効く依存
- [Dependencies in path operation decorators（FastAPI）](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/) — 戻り値を使わない依存の置き方
- [Dependencies（FastAPI）](https://fastapi.tiangolo.com/tutorial/dependencies/) — `Depends` と `Annotated` による依存性注入の基礎
- [Metadata and Docs URLs（FastAPI）](https://fastapi.tiangolo.com/tutorial/metadata/) — `title`/`version`/`openapi_tags` と docs URL の制御
- [APIRouter（FastAPI リファレンス）](https://fastapi.tiangolo.com/reference/apirouter/) — `APIRouter` の全パラメータ
- [FastAPI CLI](https://fastapi.tiangolo.com/fastapi-cli/) — `fastapi dev` / `fastapi run` と起動設定
