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の仕様を意識しなくて済むようにします。
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から取得したデータを使って、システムが達成すべき目的(ユースケース)を実現します。
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層(プレゼンテーション)
ユーザー(クライアント)との接点です。入力値の受け取りと、最終的なレスポンスの形成を担当します。
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モデルへのマッピング)」程度です。
このアーキテクチャのメリット
このように分割することで、開発効率と品質が向上します。
-
テストが容易になる
- Service層のテストを書く際、Repositoryをモック(偽物に差し替え)すれば、実際のDBを用意せずにロジックの検証が可能です。
-
仕様変更に強い
- 「レスポンスの形式を変えたい」ならRouterだけ、「DBの種類を変えたい」ならRepositoryだけを修正すればよく、他の層への影響を最小限に抑えられます。
-
チーム開発がスムーズ
- 「AさんはAPIの定義(Router)を作って、Bさんは裏側のロジック(Service)を実装する」といった分担がやりやすくなります。
まとめ
- Repository: DB操作(SQL)を隠蔽する。
- Service: 業務ロジック(判定・計算)を集約する。
- Router: 入出力を管理し、Serviceにつなぐ。
小規模なアプリでは「面倒だな」と感じるかもしれませんが、機能が増えてくるにつれて、この整理整頓がボディブローのように効いてきます。
「コードが散らかってきたな」と感じたら、ぜひこのディレクトリ構成にリファクタリングしてみてください。
Discussion