fastapi-best-practices について
エージェントに FastAPI コードの現行プラクティスを示し、古い書き方を避けるためのスキルです。エンドポイント設計、依存関係、バリデーション、アプリのライフサイクル、構成を扱います。
主な機能
- 01処理内容に応じた非同期・同期エンドポイントの選択
- 02Depends と Annotated による依存関係管理
- 03Pydantic v2 のパターン適用
- 04lifespan による起動と終了の管理
- 05APIRouter によるルート整理
- 06レスポンス検証と設定管理
活用シーン
- 01I/O 中心の FastAPI エンドポイントを実装
- 02ルートを APIRouter モジュールに再構成
- 03非推奨の Pydantic v1 パターンを置き換え
- 04FastAPI コードの一般的なアンチパターンを確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ ofershap/ fastapi-best-practices --skill fastapi-best-practicesインストール後、Claude Code で/fastapi-best-practicesを入力して読み込まれたか確認します。
詳細
使用する場面
FastAPI のコードを扱うときに、このスキルを使用してください。最新のベストプラクティスを学び、AI エージェントが古いパターンを使って犯しがちなミスを防ぎます。
重要なルール
1. I/O 待ちのエンドポイントには async def、CPU 負荷の高い処理には def を使う
誤り(エージェントがよく行う例):
@app.get("/users")
def get_users():
users = db.query(User).all()
return users
@app.get("/data")
async def get_data():
result = heavy_computation()
return result
正しい例:
@app.get("/users")
async def get_users(db: AsyncSession = Depends(get_db)):
result = await db.execute(select(User))
return result.scalars().all()
@app.get("/data")
def get_data():
return heavy_computation()
理由: FastAPI は非同期エンドポイントをイベントループで実行し、同期エンドポイントをスレッドプールで実行します。ブロッキングを避けるため、I/O(DB、HTTP、ファイル)には async を使います。CPU 負荷の高い処理には def を使います。これを async にすると、イベントループをブロックしてしまいます。
2. 依存性注入には Depends() を使う
誤り(エージェントがよく行う例):
db = get_database()
@app.get("/items")
async def get_items():
return db.query(Item).all()
正しい例:
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.get("/items")
async def get_items(db: Annotated[Session, Depends(get_db)]):
return db.query(Item).all()
理由: グローバルな DB 接続はリークが発生し、テストが難しく、FastAPI の依存性システムも迂回してしまいます。Depends() を使うと、適切なスコープ管理、クリーンアップ、テスト時のオーバーライドが可能になります。
3. Pydantic v2 のパターンを使う
誤り(エージェントがよく行う例):
from pydantic import validator
class Item(BaseModel):
name: str
price: float
class Config:
orm_mode = True
@validator("price")
def price_positive(cls, v):
if v <= 0:
raise ValueError("must be positive")
return v
正しい例:
from pydantic import BaseModel, field_validator, ConfigDict
class Item(BaseModel):
model_config = ConfigDict(from_attributes=True)
name: str
price: float
@field_validator("price")
@classmethod
def price_positive(cls, v: float) -> float:
if v <= 0:
raise ValueError("must be positive")
return v
理由: Pydantic v1 の validator、Config、orm_mode は非推奨です。field_validator、model_validator、ConfigDict、from_attributes を使ってください。
4. lifespan コンテキストマネージャーを使う
誤り(エージェントがよく行う例):
@app.on_event("startup")
async def startup():
app.state.db = await create_pool()
@app.on_event("shutdown")
async def shutdown():
await app.state.db.close()
正しい例:
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.db = await create_pool()
yield
await app.state.db.close()
app = FastAPI(lifespan=lifespan)
理由: on_event は非推奨です。lifespan コンテキストマネージャーを使うと、リソースの順序を適切に管理しながら、起動と終了の処理を一箇所にまとめられます。
5. fire-and-forget の処理には BackgroundTasks を使う
誤り(エージェントがよく行う例):
@app.post("/send-email")
async def send_email(email: str):
asyncio.create_task(send_email_async(email))
return {"status": "queued"}
正しい例:
@app.post("/send-email")
async def send_email(email: str, background_tasks: BackgroundTasks):
background_tasks.add_task(send_email_async, email)
return {"status": "queued"}
理由: asyncio.create_task はリクエストの終了後も実行され続ける可能性があり、シャットダウン時に待機されません。BackgroundTasks はレスポンス送信後に実行され、リクエストのライフサイクルに紐づきます。
6. ルートの整理には APIRouter を使う
誤り(エージェントがよく行う例):
# main.py - 500 lines of routes
@app.get("/users")
@app.get("/users/{id}")
@app.post("/items")
@app.get("/items")
正しい例:
# main.py
app.include_router(users.router, prefix="/users", tags=["users"])
app.include_router(items.router, prefix="/items", tags=["items"])
# routers/users.py
router = APIRouter()
@router.get("/")
@router.get("/{id}")
理由: すべてを単一ファイルにまとめたアプリは保守が困難になります。APIRouter を使うと、routers/、models/、services/、dependencies/ のような構成にできます。
7. 出力の検証には response_model を使う
誤り(エージェントがよく行う例):
@app.get("/items/{id}")
async def get_item(id: int):
item = await db.get(Item, id)
return {"id": item.id, "name": item.name}
正しい例:
@app.get("/items/{id}", response_model=ItemOut)
async def get_item(id: int, db: Session = Depends(get_db)):
item = await db.get(Item, id)
if not item:
raise HTTPException(status_code=404)
return item
理由: 生の dict は検証と OpenAPI を迂回してしまいます。response_model を使うと、スキーマの一貫性、シリアル化、ドキュメントが保証されます。
8. ステータスコードには fastapi.status を使う
誤り(エージェントがよく行う例):
raise HTTPException(status_code=404, detail="Not found")
raise HTTPException(status_code=401, detail="Unauthorized")
正しい例:
from fastapi import status
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found")
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Unauthorized")
理由: マジックナンバーはエラーの原因になりやすいです。ステータス定数はコードの意図を明確にし、HTTP 仕様にも準拠します。
9. 依存関係には Annotated を使う
誤り(エージェントがよく行う例):
@app.get("/me")
async def read_me(current_user: User = Depends(get_current_user)):
return current_user
正しい例:
@app.get("/me")
async def read_me(current_user: Annotated[User, Depends(get_current_user)]):
return current_user
理由: Annotated は FastAPI で推奨されるパターンです。型と依存関係を一箇所にまとめられ、依存関係の再利用にも対応します。
10. 設定には pydantic-settings を使う
誤り(エージェントがよく行う例):
import os
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///db.sqlite")
正しい例:
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str = "sqlite:///db.sqlite"
debug: bool = False
model_config = {"env_file": ".env"}
settings = Settings()
理由: os.getenv には検証機能も型情報もありません。BaseSettings を使うと、検証、.env の読み込み、型安全性が得られます。
パターン
- prefix と tags を指定して routers/ にルーターを定義する
- 共通の依存関係は dependencies.py に置く
- エラーにはステータス定数を指定した HTTPException を使う
- 検証(min_length、ge、le)を指定して Path、Query、Body、Header を使う
- app.add_exception_handler でカスタム例外ハンドラーを登録する
- ミドルウェアは必要最小限にする。順序が重要(最初に追加したものはリクエスト時に最後に実行されます)
アンチパターン
- @app.on_event("startup") や @app.on_event("shutdown") を使わない
- リクエスト単位のバックグラウンド処理に asyncio.create_task を使わない
- DB、キャッシュ、設定にグローバル変数を使わない
- Pydantic v1 の @validator や class Config を使わない
- response_model を指定せずに生の dict を返さない
- ステータスコードにマジックナンバーを使わない
- すべてのルートを main.py に記述しない