🙄

FastAPIで実践する「レイヤードアーキテクチャ」の基本設計

に公開

*この記事はAIを用いて作成しています。

FastAPIはシンプルに書けるのが魅力ですが、開発が進むにつれて main.py やルーター関数が肥大化し、**「どこに何が書かれているか分からない」**という状態になりがちです。

今回は、実務でもよく採用される「Router / Service / Repository」の3層構造について、FastAPIの実装パターンをベースに解説します。


なぜレイヤー(層)に分けるのか?

コードを機能ごとに分割配置する主な理由は、**「関心の分離(Separation of Concerns)」**を実現するためです。

すべての処理を1つの関数に書くと、以下のような弊害が起きます。

  • 可読性の低下: バリデーション、ビジネスロジック、DB操作が混在し、処理の流れが追いにくい。
  • テストの困難: ロジックだけをテストしたいのに、DB接続が必須になってしまう。
  • 保守性の低下: DBのカラム名を変更しただけで、APIのレスポンス定義まで修正が必要になるなど、影響範囲が広がる。

これらを解決するために、役割(責務)を明確に定義した「層」を作ります。


アーキテクチャの全体像

FastAPIを用いたWeb API開発では、一般的に以下の3層(+データモデル)に分割します。

レイヤー 名称 主な責務
Presentation Router HTTPリクエストの受付・バリデーション・レスポンス返却
Business Logic Service 業務ロジック・計算・判定・トランザクション制御
Data Access Repository データベースへのCRUD操作(SQL/ORMの隠蔽)

依存関係(誰が誰を呼ぶか)は以下のようになります。基本的には上から下への一方通行です。

それでは、各レイヤーの実装詳細を見ていきましょう。


1. Repository層(データアクセス)

最下層にあたるRepositoryの責務は、「データの永続化」です。
SQLAlchemyなどのORM操作はここに閉じ込め、上位レイヤー(Service)がSQLやDBの仕様を意識しなくて済むようにします。

repositories/user_repository.py
from sqlalchemy.orm import Session
from models import User as UserModel

class UserRepository:
    def get_by_id(self, db: Session, user_id: int) -> UserModel | None:
        """IDを指定してユーザーを取得する(DB操作のみ)"""
        return db.query(UserModel).filter(UserModel.id == user_id).first()

    def create(self, db: Session, name: str, email: str) -> UserModel:
        """ユーザーを新規作成する"""
        new_user = UserModel(name=name, email=email)
        db.add(new_user)
        db.commit()
        db.refresh(new_user)
        return new_user

ポイント: ここには「ビジネスロジック」を含めません。例えば「管理者権限がないと作成できない」といった判定は、ここではなくService層で行います。


2. Service層(ビジネスロジック)

アプリケーションの中核となる層です。Repositoryから取得したデータを使って、システムが達成すべき目的(ユースケース)を実現します。

services/user_service.py
from fastapi import HTTPException
from sqlalchemy.orm import Session
from repositories.user_repository import UserRepository

class UserService:
    def __init__(self):
        # 依存するRepositoryを初期化
        self.user_repo = UserRepository()

    def get_user_info(self, db: Session, user_id: int):
        """
        ユーザー情報を取得するビジネスロジック
        """
        # 1. Repositoryを利用してデータを取得
        user = self.user_repo.get_by_id(db, user_id)
        
        # 2. 業務的な判定(存在チェックなど)
        if not user:
            # 存在しない場合はビジネスロジックとしてエラーを確定させる
            raise HTTPException(status_code=404, detail="User not found")
        
        # 3. 必要であればデータの加工を行って返す
        return user

ポイント: リクエストボディがどう来るかなどは関知せず、純粋なPythonの関数として振る舞います。これにより、このServiceをCLIツールやバッチ処理から再利用することも可能になります。


3. Router層(プレゼンテーション)

ユーザー(クライアント)との接点です。入力値の受け取りと、最終的なレスポンスの形成を担当します。

routers/user_router.py
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from db import get_db
from schemas import UserResponse
from services.user_service import UserService

router = APIRouter()

@router.get("/users/{user_id}", response_model=UserResponse)
def read_user(
    user_id: int, 
    db: Session = Depends(get_db)
):
    """
    エンドポイント定義
    """
    # Serviceをインスタンス化して処理を委譲
    user_service = UserService()
    
    # ロジックの実行結果を受け取る
    return user_service.get_user_info(db, user_id)

ポイント: Router関数の中身は極力シンプルに保ちます。ここで行うのは「Serviceを呼ぶこと」と「型変換(Pydanticモデルへのマッピング)」程度です。


このアーキテクチャのメリット

このように分割することで、開発効率と品質が向上します。

  1. テストが容易になる
    • Service層のテストを書く際、Repositoryをモック(偽物に差し替え)すれば、実際のDBを用意せずにロジックの検証が可能です。
  2. 仕様変更に強い
    • 「レスポンスの形式を変えたい」ならRouterだけ、「DBの種類を変えたい」ならRepositoryだけを修正すればよく、他の層への影響を最小限に抑えられます。
  3. チーム開発がスムーズ
    • 「AさんはAPIの定義(Router)を作って、Bさんは裏側のロジック(Service)を実装する」といった分担がやりやすくなります。

まとめ

  • Repository: DB操作(SQL)を隠蔽する。
  • Service: 業務ロジック(判定・計算)を集約する。
  • Router: 入出力を管理し、Serviceにつなぐ。

小規模なアプリでは「面倒だな」と感じるかもしれませんが、機能が増えてくるにつれて、この整理整頓がボディブローのように効いてきます。
「コードが散らかってきたな」と感じたら、ぜひこのディレクトリ構成にリファクタリングしてみてください。

Discussion