FastAPIのファイル構成入門:役割分担と設計時の注意点

woman in an office looking through documents on her lap
Photo by Mizuno K on Pexels.com

FastAPIは小さなAPIならmain.pyだけでも始められますが、機能が増えるにつれて、変更する場所やテストする範囲が分かりにくくなります。

そこで、ファイル数を増やすこと自体を目的にせず、HTTPの受付、データ検証、データベース操作、設定、テストという役割ごとにコードを分けます。

この記事では、基本構成を示したうえで、各ファイルの責任と分割の判断基準をコード例で説明します。

FastAPIプロジェクトを分割する基準

プロジェクト構成に唯一の正解はなく、同じ理由で変更されるコードを近くに置き、異なる理由で変更されるコードを分けるのが出発点です。

たとえば、URLやHTTPステータスを変える作業と、データベースのテーブル定義を変える作業は目的が異なるため、ルーターとモデルを別の場所に置くと変更範囲を追いやすくなります。

基本的なディレクトリ構造

my_fastapi_project/
├── app/
│   ├── __init__.py
│   ├── main.py              # アプリケーションの組み立てと起動点
│   ├── routers/             # URLごとの受付処理
│   │   ├── __init__.py
│   │   ├── users.py
│   │   └── items.py
│   ├── schemas/             # リクエストとレスポンスの型
│   │   ├── __init__.py
│   │   └── user.py
│   ├── models/              # データベースのテーブルに対応するモデル
│   │   ├── __init__.py
│   │   └── user.py
│   ├── services/            # 必要に応じて業務処理を分離
│   │   └── user.py
│   ├── database.py          # エンジンとセッションの設定
│   ├── dependencies.py      # 共通の依存関係
│   └── config.py            # 環境変数から読み込む設定
├── tests/
│   └── test_main.py
├── .env
├── .gitignore
├── requirements.txt
├── README.md
└── Dockerfile

services/は必須ではなく、ルーターに認証以外の判断や複数の処理手順が増えてきたときに追加すれば十分です。

各ファイルとディレクトリの責任

FastAPIプロジェクトの主な構成要素
場所 担当すること 置かないほうがよいもの
main.py FastAPIインスタンスの作成とルーターの登録 個別機能の長い処理
routers/ URL、HTTPメソッド、入力、出力の受付 複雑な業務判断や直接の接続設定
schemas/ Pydanticによる入力と出力の形の定義 データベース接続処理
models/ SQLAlchemyなどによるテーブル構造の定義 HTTPレスポンスの組み立て
services/ 複数の処理を組み合わせる業務ロジック URLやHTTPステータスの定義
database.py エンジン、セッション、基底クラスの設定 機能別のAPI処理
dependencies.py データベースセッションなどの生成と後始末 特定の画面や機能だけで使う処理
config.py 環境変数の読み込みと型検証 秘密情報の直書き
tests/ エンドポイントや機能の期待動作の確認 本番で実行するアプリケーション処理

リクエストからレスポンスまでの流れ

役割の境界は、APIがリクエストを受け取って返すまでの順序で考えると理解しやすくなります。

  1. routers/がURLとHTTPメソッドに対応する処理を受け持ちます。
  2. schemas/のPydanticモデルが入力データを検証します。
  3. 必要に応じてservices/が業務処理を進め、models/を通じてデータを扱います。
  4. ルーターがレスポンス用スキーマに沿ったデータを返します。

この分け方により、HTTPの仕様を変える作業と、保存方法や業務ルールを変える作業を切り分けやすくなります。

最小構成から始めるコード例

main.pyはアプリケーションを組み立てる

main.pyではFastAPIインスタンスを作り、機能別のルーターを登録します。

from fastapi import FastAPI

from app.routers import items, users

app = FastAPI()

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


@app.get("/")
def read_root():
    return {"message": "Welcome to FastAPI!"}

APIRouterは、関連するエンドポイントをひとまとまりにし、FastAPIアプリケーションへ登録するための仕組みです。

routers/はHTTPの受付を担当する

ユーザー関連のURLをusers.pyにまとめると、ユーザー機能の入口を一つのファイルから確認できます。

from fastapi import APIRouter

from app.schemas.user import UserResponse

router = APIRouter(prefix="/users", tags=["users"])


@router.get("/", response_model=list[UserResponse])
def get_users():
    return [
        {"id": 1, "name": "Alice"},
        {"id": 2, "name": "Bob"},
    ]

ルーターが長くなったら、単に行数で分けるのではなく、ユーザー、商品、注文など、変更の単位になる機能で分けます。

schemas/はAPIで受け渡す形を定義する

スキーマは、APIが受け取るデータと返すデータの項目および型を示す契約です。

from pydantic import BaseModel


class UserCreate(BaseModel):
    name: str


class UserResponse(BaseModel):
    id: int
    name: str

作成時の入力と返却時の出力を分けると、クライアントから受け取らないIDなどを明確にできます。

入力検証と出力定義を詳しく確認したい場合は、FastAPIとPydanticによるリクエストとレスポンスの定義も参考になります。

models/は保存形式を定義する

ORMモデルは、Pythonのクラスとデータベースのテーブルを対応させるための定義です。

from sqlalchemy import Integer, String
from sqlalchemy.orm import Mapped, mapped_column

from app.database import Base


class User(Base):
    __tablename__ = "users"

    id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
    name: Mapped[str] = mapped_column(String, index=True)

スキーマは外部との受け渡しを、ORMモデルは保存形式を表すため、項目が似ていても同じ責任ではありません。

データの作成、取得、更新、削除まで実装する場合は、FastAPIとSQLiteおよびSQLAlchemyによるCRUD実装で処理のつながりを確認できます。

database.pyは接続設定をまとめる

接続先、エンジン、セッション生成、ORMモデルの基底クラスを一か所にまとめます。

from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase, sessionmaker

DATABASE_URL = "sqlite:///./test.db"

engine = create_engine(
    DATABASE_URL,
    connect_args={"check_same_thread": False},
)
SessionLocal = sessionmaker(bind=engine, autoflush=False)


class Base(DeclarativeBase):
    pass

connect_argsはこのSQLiteの例に合わせた設定であり、別のデータベースへ置き換えるときは接続方法も見直します。

dependencies.pyは生成と後始末をそろえる

依存性注入は、エンドポイントが必要とするオブジェクトを関数の外で用意し、FastAPIに受け渡しを任せる仕組みです。

from collections.abc import Generator

from sqlalchemy.orm import Session

from app.database import SessionLocal


def get_db() -> Generator[Session, None, None]:
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

yieldまででセッションを渡し、finallyで閉じることで、正常終了と例外発生のどちらでも後始末を実行できます。

config.pyは設定値を型付きで読む

環境ごとに変わる値はコードへ直書きせず、設定クラスを通して読み込みます。

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    database_url: str = "sqlite:///./test.db"

    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
    )


settings = Settings()

ローカル環境の.envには、たとえば次のように値だけを置きます。

DATABASE_URL=sqlite:///./test.db

.env.gitignoreへ追加し、実際の認証情報や秘密情報をリポジトリへ登録しないようにします。

環境ごとの切り替えや秘密情報の扱いを広げる場合は、FastAPIとpydantic-settingsによる設定管理を参照してください。

tests/は外から見える動作を確認する

FastAPIのTestClientを使うと、実際のHTTPリクエストに近い形でエンドポイントの応答を検証できます。

from fastapi.testclient import TestClient

from app.main import app

client = TestClient(app)


def test_read_root():
    response = client.get("/")

    assert response.status_code == 200
    assert response.json() == {"message": "Welcome to FastAPI!"}

テストでは成功例だけでなく、入力不足、存在しないデータ、権限不足など、APIが明示的に扱う失敗も確認します。

設計時に確認したいポイント

機能ではなく責任が混ざっていないか

ルーターがデータベース接続を作り、入力検証を行い、業務判断まで抱えると、一つの変更が広い範囲へ影響します。

まずHTTPの受付と保存処理を分け、複数の手順をまとめる必要が生じた段階でサービス層を追加します。

スキーマとモデルを混同していないか

スキーマとORMモデルの両方にidnameがあっても、前者はAPI契約、後者は保存形式という別の責任を持ちます。

同じクラスで兼用すると、テーブル変更がAPIレスポンスへ意図せず現れる可能性があるため、境界を明示します。

共通化が早すぎないか

似た処理を見つけるたびに共通ファイルへ移すと、どの機能がその処理を変更できるのか分かりにくくなります。

複数の機能が同じ規則を共有し、その規則を一か所で変更したいと確認できてから共通化します。

小規模な段階で分けすぎていないか

エンドポイントが数個しかない段階では、main.pytests/から始めても問題ありません。

ファイルを移動する目安は行数ではなく、異なる目的の変更が同じファイルに集まり、レビューやテストの範囲を判断しにくくなったときです。

構成を決めるためのチェックリスト

  • main.pyはアプリケーションの組み立てに集中しているか。
  • ルーターはユーザーや商品など、変更単位になる機能で分かれているか。
  • 入力と出力のスキーマを、データベースモデルと区別しているか。
  • データベースセッションの生成と後始末が一か所にまとまっているか。
  • 環境ごとに変わる値を設定クラスから読んでいるか。
  • .envがGit管理から除外されているか。
  • 主要な成功と失敗をtests/で再現できるか。

最初から大規模な構成をコピーするより、現在の変更理由に合わせて境界を作り、必要になった責任だけを段階的に分離するほうが保守しやすい構成になります。

投稿者 greeden

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)