FastAPI設計レビュー:非同期APIとバリデーションの適切な分離構造
はじめに
FastAPIは、非同期プログラミングとpydanticによるバリデーションを組み合わせて、非常に簡潔なAPIエンドポイントを構築できるフレームワークです。
しかしその柔軟性ゆえ、「バリデーション」「ビジネスロジック」「DBアクセス」などが1つの関数に混在しやすく、レビュー上の設計判断を難しくするケースが多く見られます。
本記事では、FastAPI設計における非同期処理とバリデーションの構造分離をレビュー視点から分析し、
可読性・責務の明確化・テスト性の向上を実現するための構造的チェックポイントを提示します。
典型例:混在したAPIハンドラ
非同期APIで責務が混在した例
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
price: float
@app.post("/items")
async def create_item(item: Item):
if item.price <= 0:
raise HTTPException(status_code=400, detail="Price must be positive")
# 仮想DBアクセス
await save_to_db(item)
return {"message": "Created"}この実装では、1つの関数に以下の責務が集中しています:
- 型定義(pydantic)
- 値検証(価格チェック)
- 例外制御(HTTPエラー)
- 非同期副作用(DBアクセス)
レビュー指摘
@Reviewer: ハンドラ関数がバリデーション・エラー制御・永続化ロジックをすべて内包しており、責務が曖昧です。
バリデーションや副作用処理は明示的に関数分離すべきです。FastAPIレビューで確認すべき責務分離の観点
- リクエストモデルの定義と検証がpydantic側に適切に移譲されているか?
HTTPExceptionなどのレスポンス処理がロジックではなく制御構造として整理されているか?- 非同期関数の中に同期処理や冗長な制御分岐が混在していないか?
- ビジネスルールの検証がモデルとインフラコードの両方に分散していないか?
改善例:バリデーション責務を分離
バリデーション関数を別定義
def validate_item(item: Item):
if item.price <= 0:
raise ValueError("Price must be positive")
async def save_item(item: Item):
await save_to_db(item)
@app.post("/items")
async def create_item(item: Item):
try:
validate_item(item)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
await save_item(item)
return {"message": "Created"}分離後の評価
@Reviewer: バリデーションが関数として分離され、非同期副作用との境界が明確になりました。
ロジックごとのテスト容易性・再利用性・トレーサビリティが改善されています。構造分離の設計図
この図は、クライアント→ハンドラ→バリデーション→永続化の責務分離構造を表しています。
レビューアーはこのような責任連鎖がコードに現れているかを読み取る必要があります。
:::warningning 非同期構造レビューの落とし穴
awaitで呼ばれている関数が非同期関数を装って同期処理を行っていないか?async defの内部で複雑な条件分岐やエラー処理が多重になっていないか?pydanticの中で@validatorが副作用を含む処理(ログ出力・DB参照など)をしていないか? :::
さらに構造を整理した設計
UseCaseクラスによる明確な責務分離
class CreateItemUseCase:
def __init__(self, db_gateway):
self.db = db_gateway
def validate(self, item: Item):
if item.price <= 0:
raise ValueError("Invalid price")
async def execute(self, item: Item):
self.validate(item)
await self.db.save(item)FastAPIルータ側
@app.post("/items")
async def create_item(item: Item, uc: CreateItemUseCase = Depends()):
try:
await uc.execute(item)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
return {"message": "Created"}レビュー補足
@Reviewer: バリデーション、非同期処理、エラー制御が構造的に分離されており、設計の可視性が高まっています。
この構造により、ユースケース単位でのテストが可能になり、保守性も向上します。結論:FastAPIレビューは「関数の短さ」ではなく「責務の境界線」を見る
FastAPIは1関数で多くの処理が書けてしまうため、構造が意図せず過密化しやすいのが特徴です。
レビューアーは以下の点を意識して構造を読み解く必要があります:
- APIエンドポイントに何個の責務が混在しているかを見極める
- 非同期関数内での同期的副作用が含まれていないかを確認する
- pydanticモデルに対し、構造定義と変換・検証・副作用が混在していないかを評価する
- バリデーション→ユースケース→レスポンスという構造的な流れが追えるかどうか
FastAPIは機能的には非常に簡潔ですが、設計としての健全性は関数の短さではなく構造の明瞭さにあります。
レビューアーの視点から、構造を立体的に読み解く力が、品質を支える重要な判断軸となります。
