👻
FastAPI × DDD における モデル設計の課題と解決策
1. はじめに
FastAPI を使って DDD (Domain-Driven Design) のアーキテクチャを採用すると、
データの永続化 (infra
層) と request
のバリデーション (router
層) で 異なる目的のモデルが必要 になります。
しかし、
- どこまでモデルを分けるべきか?
- リクエストモデルとドメインモデルを統一できないか?
- 永続化処理で
id
を扱うべきか?
といった 設計の迷い が出てきます。
本記事では、DDD を適用する中で直面したモデル設計の課題とその解決策 について整理していきます。
2. 課題と解決策
id
を含めるべきか?
課題 ① DDD では id
を Optional
にするかどうか で悩むことが多いです。
たとえば、以下の User
モデルを考えてみます。
User
モデル
📝 ユーザーを表す from typing import Optional
from pydantic import BaseModel
class User(BaseModel):
id: Optional[int] # ID はオプショナル
name: str
age: int
このモデルを POST
のリクエストとして使うと、Swagger で id
が表示されてしまう という問題があります。
id
をリクエストモデルから分離
解決策 ① FastAPI の Swagger
に id
を含めたくない場合、リクエスト専用の CreateUserRequest
を作る ことで解決できます。
Request
モデル
📝 class CreateUserRequest(BaseModel):
name: str
age: int
🎯 メリット
POST /users
のリクエストにid
が表示されなくなる- リクエストバリデーションの責務を
Request
モデルに分離できる - リクエストの仕様が明確になる
Create
/ Update
のモデルが増えすぎる
課題 ② CreateUserRequest
を作ると、UpdateUserRequest
も必要になる という問題があります。
UpdateUserRequest
を作成
📝 class UpdateUserRequest(BaseModel):
id: int # 更新時は ID が必須
name: str
age: int
✅ 問題点
-
CreateUserRequest
/UpdateUserRequest
でほぼ同じフィールドを持つ - API が増えると、
Create
/Update
/Delete
ごとに似たようなモデルが増えてしまう
BaseUser
を作成し、Create
/ Update
を継承
解決策 ② リクエストの共通部分 (name
, age
) を BaseUser
にまとめることで、
モデルの重複を減らすことができます。
BaseUser
を作成
📝 class BaseUser(BaseModel):
name: str
age: int
class CreateUserRequest(BaseUser):
pass # ✅ ID は含まない
class UpdateUserRequest(BaseUser):
id: int # ✅ ID は必須
🎯 メリット
-
Create
/Update
の重複を減らせる -
BaseUser
に共通のバリデーションロジックを定義できる -
Request
モデルの構造が明確になる
infra
層のインターフェースでどのモデルを使うべきか?
課題 ③ DDD では infra
層は データベースへのアクセスのみを責務とする ため、
-
infra
層でpydantic
を使うのか? -
infra
層のインターフェース (IUserRepository
) でCreateUserRequest
などを使うべきか? -
id
を使わないcreate
でもid
を含めたUser
を使うべきか?
といった疑問が生まれます。
infra
層は User
モデルを統一的に使用
解決策 ③ infra
層では 作成 (create_user
) でも User
を使う が、
id
は Optional にして infra
層のロジックで使うか決める。
domain/model/user.py
📝 class User:
def __init__(
self,
id: Optional[int] = None, # `id` を含めるが、使うかどうかは `infra` 層の責務
name: str = "",
age: int = 0,
):
self.id = id
self.name = name
self.age = age
✅ このモデルを infra
層のリポジトリで使用する
リクエストの変換
router
では CreateUserRequest
を User
に詰め替える。
from fastapi import APIRouter, Depends
from app.domain.repository.user import IUserRepository
from app.domain.model.user import User, CreateUserRequest, UpdateUserRequest
router = APIRouter()
def to_create_model(request: CreateUserRequest) -> User:
"""リクエストモデルを `User` に変換"""
return User(name=request.name, age=request.age)
@router.post("/users", response_model=User)
async def create_user(
request: CreateUserRequest,
user_repo: IUserRepository = Depends()
):
user = to_create_model(request)
return user_repo.create_user(user)
🎯 メリット
infra
層はUser
だけを扱うので統一感が出る- リクエストを
infra
のモデルに詰め替えるのはrouter
の責務とする - FastAPI の
Swagger
に不要なid
が表示されない
📌 まとめ
課題 | 解決策 | メリット |
---|---|---|
id を POST で表示させたくない |
CreateUserRequest を作る |
Swagger で id を非表示 |
Create / Update モデルが増える |
BaseUser を作り、Create / Update を継承 |
モデルの重複を減らせる |
infra 層でどのモデルを使うか |
User を統一的に使用し、id の扱いは infra で決める |
infra の repository の一貫性を保てる |
Request
モデル (CreateUserRequest
/UpdateUserRequest
) を作成するinfra
層はUser
を統一的に使用し、id
の扱いはinfra
の責務とするrouter
でRequest
→User
に変換してrepository
に渡す- この設計により、Swagger の問題を回避しつつ、
infra
のrepository
も統一的に管理できる
Discussion