メインコンテンツへスキップ
友田 陽大
Pythonバックエンド
Python
FastAPI
アーキテクチャ設計
依存性注入
保守性

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

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

公開日
読了時間
21分
著者
友田 陽大
シェア

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

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

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


0. まず判断:いつ分割するのか(YAGNI)

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

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

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

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


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

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

# 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. 公式推奨のプロジェクト構成

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

.
├── 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箇所にまとめます。

# 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 を参照するときは相対インポートを使います。

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

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


3. include_router:機能を本体に組み込む

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

# 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 / responsesrouter を作るとき(APIRouter(...))にも、組み込むとき(include_router(...))にも指定できます。「その機能に固有の設定」は router 側、「組み込み先の事情で足す設定」は include 側、と使い分けます。

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

3.1 router を router に入れ子にする

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

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 ① グローバル依存:全エンドポイント共通の前提

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

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

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

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

@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 に一度だけ付ければ、書き忘れが構造的に起きません

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(経産大臣賞受賞)では、業種ベースの認可をルーター層に一元化し、全 221 エンドポイントを一貫したポリシーで保護——第三者ペネトレで認証欠落0件を実証しています。認可は「各ハンドラの善意の if 文」ではなく、境界に一元化された構造で守るのが本番品質です(→認証・認可ガイド)。


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

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

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

  • router(HTTP境界):リクエスト/レスポンスの変換だけ。薄く保つ
  • service(ビジネスロジック):業務ルール。HTTP も DB ドライバも知らない
  • repository(I/O):DB・外部 API への入出力だけ。
# 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
# 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)
# 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)]
# 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 ガイド)。

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

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

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

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 ...)です。原因はほぼ常に「依存の向きが双方向になっている」こと。これは規律で根絶できます。

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

main → api(routers) → services → repositories → models / schemas
                  ↘ core(config / db / security) ← 誰からでも参照してよい土台
  • 上位は下位を import してよい。下位は上位を import してはいけない。 repositoryrouter を import した時点で循環の芽です。
  • core/(設定・DB・セキュリティ)は最下層の土台として、誰からでも import される側に置く。core は上位を import しない。
  • main.py を他モジュールから import しない。 router は app を必要としません(APIRouter を使う)。「router が app を欲しがる」設計になったら、それは依存の向きが逆——include_router組み込む側(main)から差し込むのが正しい向きです。
  • 型ヒントのためだけの相互参照は、実行時 import を避けて解消します。
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 に入れ子」がそのまま使えます。

# 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"])
# 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 した瞬間に副作用(設定読み込み・接続)が走り、テストで差し替えにくくなります。これをファクトリ関数に畳むと、組み立てが一望でき、テストごとに新鮮なアプリを作れます。

# 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仕様書になります。

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.jsonAPI の全構造(全エンドポイント・全スキーマ)を公開します。社内 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 を駆動)。

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

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

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

本番のワーカー多重化・グレースフルシャットダウンは本番運用ガイドの第9章に従ってください(プロセスマネージャ+複数ワーカー、lifespan での後始末)。

10.2 テスト:router 単位 + dependency_overrides

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

# 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.pydependencies.pyrouters/internal/)。各 dir に __init__.py。相対インポート(. / ..)。router はモジュールごと import して名前衝突を避ける。
  • include_routerprefix/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→modelscore は土台)。型のみ参照は TYPE_CHECKING
  • バージョニング/api/v1 を router 入れ子で束ね、/api/v2 を並走。共通ロジックは service で共有。完全独立なら mount
  • docsopenapi_tags の順序で並べる。本番は docs_url=None 等で露出を絞る
  • 実行/テストfastapi dev(開発)/fastapi run(本番)、[tool.fastapi] entrypointcreate_app ファクトリ+dependency_overrides で隔離テスト。

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

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

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


参考(公式ドキュメント)

友田

友田 陽大

経済産業大臣賞 受賞プロダクト開発者。TypeScript + Python + AWS で、SaaS・業界DX・ 実用レベルの生成AI(RAG)を、要件定義からインフラ・運用まで一人で完遂します。

この記事で解説した技術の適用事例

国内大手放送事業者向け社内AIプラットフォーム(複数のFastAPIサービスを単一の規約で束ねるモノレポを構築)

ケーススタディを見る