👻
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