FastAPIはPythonフレームワークの中でも特にopinionatedな設計を持っている——それはポジティブな意味で。依存性注入、非同期ルートハンドラ、Pydanticモデル、型アノテーションについて明確なパターンが存在する。そしてそのパターンは、AIコーディングエージェントが明示的な指示なしに無視するか、微妙にずらして実装してしまう種類のものでもある。
デフォルト状態では、FastAPIプロジェクトでClaude CodeやCursorを使うと以下のことが起きる:
async defとdefのルートハンドラをパフォーマンス上の影響を理解せずに混在させる- 何も指示がないからという理由でPydanticモデルとSQLAlchemyモデルを同じファイルに置く
- 非同期エンドポイントに対して
AsyncClientが必要なのにTestClientを使う - 既存の
OAuth2PasswordBearer設定とマッチしない認証ミドルウェアを生成する Depends()で既存の依存関係を使う代わりに新しいユーティリティ関数を作る
プロジェクトルートにAGENTS.mdを置くと、これらがすべて解消される。AGENTS.mdは人間向けのドキュメントではない——エージェントがコードを1行書く前に、あなたのコードベースの理解を形作る機械可読な契約書だ。
なぜFastAPIには特にAGENTS.mdが必要か
多くのPythonウェブフレームワークは緩い規約を持っている。FastAPIは違う。フレームワークはそのパターンを正確に守ることで報われる設計になっている:非同期優先のルートハンドラ、すべての境界でのPydanticバリデーション、共有ロジックのための依存性注入、そしてファーストクラスの出力としてのOpenAPIスキーマ。
エージェントがこれらのパターンを知らないと、汎用的なPythonの慣習にフォールバックする。間違ってはいないが、FastAPIではない。技術的には動くが、プロジェクトにフィットしないコードのレビューに時間を取られることになる。
FastAPIを特にインストルメンションする価値がある3つの理由:
非同期は実質的に全か無かだ。 async defルートの中で同期データベースコールを呼ぶと、イベントループ全体がブロックされる。DBレイヤーが非同期かどうかをエージェントが知らないと、テストは通るが本番で負荷時に落ちるブロッキングコードが生まれる。
依存性注入がAPIそのものだ。 FastAPIのDepends()システムはデータベースセッション、認証、レート制限、共有設定を処理するフレームワークの仕組みだ。依存関係の階層を知らないエージェントは直接インスタンス化でそれを迂回するか、ミドルウェアを壊す並行実装を作る。
Pydanticモデルは2つの別の役割を持つ。 リクエスト/レスポンススキーマ(APIの境界でのバリデーション)とORMモデル(データベースの表現)は似て見えるが、決して混在させてはいけない。明示的な指示なしにエージェントはこれらを一緒にする——特に意図的にその境界を曖昧にするSQLModelを使う場合に顕著だ。
FastAPIプロジェクトにおけるAGENTS.md vs CLAUDE.md
この2つのファイルは互換ではない。目的を混同することはよくある間違いだ。
AGENTS.mdはエージェントコンテキストファイルだ。AIエージェントにコードベースでの振る舞いを伝える:ファイルの場所、実行すべきコマンド、コード生成時に従うパターン、避けるべきこと。Claude Code、OpenAI Codex、Cursor、そしてAGENTS.md仕様をサポートするあらゆるエージェントが読む。技術的で規定的な内容を書く。
CLAUDE.md(Claude Code専用)はその上にClaudeに固有の振る舞いを加える:スラッシュコマンドの定義、メモリのヒント、プロジェクト固有の設定、Claudeへの説明スタイルの指示。「迷ったら認証コードに触る前に確認する」とか「このプロジェクトは特定のマイグレーション命名規則に従っている」といった内容はここに書くべきだ。
単独の開発者か小規模チームのFastAPIプロジェクトなら、多くの場合AGENTS.md一つで十分カバーできる。CLAUDE.md は、他のエージェントには適用されないClaude固有のワークフローがある場合、またはレビューの好みやコミュニケーションスタイルを記録したい場合に追加する。
合理的な分担:
AGENTS.md → プロジェクトアーキテクチャ、パターン、コマンド、規約
CLAUDE.md → ワークフローの好み、質問の振る舞い、レビュースタイル、ショートカット
エンコードすべきプロジェクト構造パターン
AGENTS.mdのプロジェクト構造セクションは最も重要な部分だ。FastAPIプロジェクトはほとんどのフレームワークよりもバリエーションが多い——Rails流の強制的なディレクトリレイアウトは存在しない——なので、エージェントには明示的なガイダンスが必要だ。
スケールしやすい構造の例:
project/
├── app/
│ ├── main.py # FastAPIアプリのインスタンス化、ミドルウェア、startupイベント
│ ├── dependencies.py # 共有Depends()関数(DBセッション、現在ユーザー等)
│ ├── config.py # pydantic-settings BaseSettingsクラス
│ ├── api/
│ │ ├── v1/
│ │ │ ├── router.py # APIRouter、include_router()呼び出し
│ │ │ └── endpoints/
│ │ │ ├── users.py
│ │ │ └── items.py
│ │ └── v2/ # 将来バージョン、同じ構造
│ ├── models/
│ │ └── user.py # SQLAlchemy ORMモデルのみ
│ ├── schemas/
│ │ └── user.py # Pydanticリクエスト/レスポンススキーマのみ
│ ├── crud/
│ │ └── user.py # DBオペレーション、引数にdbセッションを受け取る
│ └── core/
│ ├── security.py # JWT生成、パスワードハッシュ
│ └── database.py # エンジン生成、SessionLocal、Base
├── tests/
│ ├── conftest.py # pytestフィクスチャ、テストDB設定
│ └── api/
│ └── v1/
│ └── test_users.py
├── alembic/ # データベースマイグレーション
├── AGENTS.md
└── pyproject.toml
AGENTS.mdにはこれを明示的に記述する:
## プロジェクト構造
このプロジェクトはレイヤードアーキテクチャを採用している。新しいリソースを追加する場合:
1. `app/models/<resource>.py` — SQLAlchemy ORMモデル
2. `app/schemas/<resource>.py` — Pydanticリクエスト/レスポンススキーマ
3. `app/crud/<resource>.py` — DBオペレーション(ルートロジックをここに書かない)
4. `app/api/v1/endpoints/<resource>.py` — ルートハンドラ(DBロジックをここに書かない)
5. `app/api/v1/router.py`にルーターを登録する
PydanticスキーマをmodelsディレクトリにORMモデルをschemasに置いてはいけない。
ルートハンドラに直接DBクエリを書かない——必ずcrud/を経由する。
ルーター構成とバージョニング
FastAPIのAPIRouterによるバージョニングは綺麗に書けるが、エージェントが新しいエンドポイントを追加する際に間違えやすい。AGENTS.mdでルーター組み込みのパターンを明示する:
## APIバージョニング
ルートはバージョン管理されている。新しいエンドポイントは`app/api/v1/endpoints/`に追加する。
`app/api/v1/router.py`でのルーター登録:
```python
from fastapi import APIRouter
from app.api.v1.endpoints import users, items, auth
api_router = APIRouter()
api_router.include_router(users.router, prefix="/users", tags=["users"])
api_router.include_router(items.router, prefix="/items", tags=["items"])
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
app/main.pyでは:
app.include_router(api_router, prefix="/api/v1")
結果:エンドポイントは/api/v1/users/、/api/v1/items/等で利用可能。
main.pyにルートハンドラを直接書いてはいけない。
## 依存性注入パターン
最も多くのバグを防ぐセクションだ。FastAPIの`Depends()`システムは強力で、エージェントはしばしば直接インスタンス化でそれを迂回する。AGENTS.mdで依存関係の連鎖を明示的に記述する必要がある。
```markdown
## 依存性注入
コア依存関係は`app/dependencies.py`にある。常に`Depends()`を使う——ルートハンドラで
サービスを直接インスタンス化したりDBセッションを作成してはいけない。
### データベースセッション
```python
# app/dependencies.py
from app.core.database import SessionLocal
from typing import AsyncGenerator
from sqlalchemy.ext.asyncio import AsyncSession
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with SessionLocal() as session:
try:
yield session
finally:
await session.close()
ルートでの使用例:
@router.get("/users/{user_id}")
async def get_user(
user_id: int,
db: AsyncSession = Depends(get_db)
):
...
現在のユーザー
# app/dependencies.py
from app.core.security import verify_token
from app.crud.user import get_user_by_id
async def get_current_user(
token: str = Depends(oauth2_scheme),
db: AsyncSession = Depends(get_db)
) -> User:
user_id = verify_token(token)
user = await get_user_by_id(db, user_id)
if not user:
raise HTTPException(status_code=401, detail="User not found")
return user
ルートに認証を追加する場合はget_current_userをインポートして使う——
インラインでトークン検証を再実装しない。
## 非同期パターン
エージェントが最も重大な間違いを犯す領域だ。非同期の規約を明示的に記述する。
```markdown
## 非同期ルール
**すべてのルートハンドラは`async def`でなければならない。** このプロジェクトは
非同期DBドライバー(asyncpg / aiosqlite)を使用している。同期ルートハンドラは
イベントループをブロックする。
```python
# 正しい
@router.get("/items/{item_id}")
async def get_item(item_id: int, db: AsyncSession = Depends(get_db)):
return await crud.item.get(db, item_id)
# 間違い — こう書いてはいけない
@router.get("/items/{item_id}")
def get_item(item_id: int, db: AsyncSession = Depends(get_db)):
...
CRUD関数も非同期でなければならない:
# app/crud/item.py
async def get(db: AsyncSession, item_id: int) -> Item | None:
result = await db.execute(select(Item).where(Item.id == item_id))
return result.scalar_one_or_none()
バックグラウンドタスクはfire-and-forgetの処理に使う:
BackgroundTasksはレスポンスをブロックすべきでない処理(メール送信、外部サービスへのログ等)
にのみ使用する。呼び出し元が結果を必要とする処理にはバックグラウンドタスクを使わない。
@router.post("/users/")
async def create_user(
user_in: UserCreate,
background_tasks: BackgroundTasks,
db: AsyncSession = Depends(get_db)
):
user = await crud.user.create(db, user_in)
background_tasks.add_task(send_welcome_email, user.email)
return user
## Pydanticスキーマの規約
FastAPIのリクエスト/レスポンス処理向けのPydanticモデルには、エージェントが知るべき特定のパターンがある:
```markdown
## Pydanticスキーマ規約
スキーマは`app/schemas/`に置く。`app/models/`のORMモデルとは分離する。
リソースの標準スキーマパターン:
```python
# app/schemas/user.py
from pydantic import BaseModel, EmailStr
from datetime import datetime
class UserBase(BaseModel):
email: EmailStr
full_name: str | None = None
class UserCreate(UserBase):
password: str
class UserUpdate(BaseModel):
email: EmailStr | None = None
full_name: str | None = None
class UserInDB(UserBase):
id: int
is_active: bool
created_at: datetime
model_config = ConfigDict(from_attributes=True)
class UserPublic(UserBase):
id: int
is_active: bool
UserCreate— POST時の入力ボディ(パスワードを含む)UserPublic— レスポンス出力(パスワードを含まない)UserInDB— ORM → Pydantic変換(from_attributes=Trueを使用)UserUpdate— PATCHボディ(全フィールドをOptionalに)
ルートハンドラはPydanticスキーマインスタンスを返す(FastAPIがresponse_modelで
シリアライズする):
@router.get("/users/{user_id}", response_model=UserPublic)
async def get_user(user_id: int, db: AsyncSession = Depends(get_db)):
user = await crud.user.get(db, user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user # FastAPIがresponse_model経由でシリアライズ
## データベースレイヤーのパターン
どのORMを使用しているか、セッションがどのように接続されているかを記述する:
```markdown
## データベース
このプロジェクトはSQLAlchemy 2.xの非同期サポートを使用(PostgreSQL用asyncpgドライバー)。
`app/core/database.py`のエンジンとセッションファクトリ:
```python
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase
engine = create_async_engine(settings.DATABASE_URL, echo=False)
SessionLocal = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
class Base(DeclarativeBase):
pass
ORMモデルはBaseを継承する:
# app/models/user.py
from app.core.database import Base
from sqlalchemy import String, Boolean
from sqlalchemy.orm import Mapped, mapped_column
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
hashed_password: Mapped[str] = mapped_column(String)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
マイグレーション:Alembic。モデル変更後:
alembic revision --autogenerate -m "description"- 生成されたマイグレーションを確認する
alembic upgrade head
既存のマイグレーションは絶対に修正しない。新しいものを作成する。
## テスト規約
非同期のFastAPIエンドポイントを正しくテストするには`pytest-asyncio`と`httpx.AsyncClient`が必要だ。ここはエージェントがプロジェクトの設定と合わないコードを最も多く生成する領域の一つだ。
```markdown
## テスト
テストフレームワーク:pytest with pytest-asyncio。テストクライアント:httpx AsyncClient
(StarletteのTestClientではない)。
### conftest.py構造
```python
# tests/conftest.py
import pytest
import pytest_asyncio
from httpx import AsyncClient, ASGITransport
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from app.main import app
from app.core.database import Base
from app.dependencies import get_db
TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:"
@pytest_asyncio.fixture(scope="session")
async def engine():
engine = create_async_engine(TEST_DATABASE_URL)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield engine
await engine.dispose()
@pytest_asyncio.fixture
async def db(engine):
async with async_sessionmaker(engine)() as session:
yield session
await session.rollback()
@pytest_asyncio.fixture
async def client(db):
async def override_get_db():
yield db
app.dependency_overrides[get_db] = override_get_db
async with AsyncClient(
transport=ASGITransport(app=app),
base_url="http://test"
) as ac:
yield ac
app.dependency_overrides.clear()
テストのパターン
# tests/api/v1/test_users.py
import pytest
@pytest.mark.asyncio
async def test_create_user(client: AsyncClient):
response = await client.post(
"/api/v1/users/",
json={"email": "[email protected]", "password": "secret123"}
)
assert response.status_code == 201
data = response.json()
assert data["email"] == "[email protected]"
assert "password" not in data
@pytest.mark.asyncio
async def test_get_user_not_found(client: AsyncClient):
response = await client.get("/api/v1/users/99999")
assert response.status_code == 404
テスト実行:pytest tests/ -v
カバレッジ付き:pytest tests/ --cov=app --cov-report=term-missing
AsyncClientを使う——非同期エンドポイントにTestClientを使わない。
## 認証パターン
```markdown
## 認証
このプロジェクトはOAuth2パスワードフロー付きJWTを使用している。認証ユーティリティは
`app/core/security.py`にある。
### OAuth2スキーム
```python
# app/core/security.py
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/token")
トークン生成
from datetime import datetime, timedelta, timezone
from jose import jwt
SECRET_KEY = settings.SECRET_KEY
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
def create_access_token(subject: str | int) -> str:
expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
payload = {"sub": str(subject), "exp": expire}
return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
保護されたルート
@router.get("/users/me", response_model=UserPublic)
async def get_current_user_profile(
current_user: User = Depends(get_current_user)
):
return current_user
保護されたルートには常にDepends(get_current_user)を使う。
ルートハンドラ内でトークンを手動で検証しない。
## エラーハンドリング
```markdown
## エラーハンドリング
クライアントエラーにはすべて`HTTPException`を使う。ルートハンドラ内で
生のPython例外をraiseしない。
標準パターン:
```python
from fastapi import HTTPException, status
# 404 Not Found
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
# 400 Bad Request
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered")
# 401 Unauthorized
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
# 403 Forbidden
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions")
fastapiのstatus.HTTP_*定数を使う——整数のステータスコードをハードコードしない。
バリデーションエラーはPydanticに処理させる。FastAPIはRequestValidationErrorを
自動的に422に変換する——カスタムエラーフォーマットが必要な場合を除いて、
バリデーションエラーをキャッチして再raiseしない。
予期しないサーバーエラーはそのまま伝播させる。ルートハンドラで汎用Exceptionを
キャッチしない。カスタム500レスポンスが必要ならmain.pyにグローバル例外ハンドラを追加する。
## pydantic-settingsによる設定管理
```markdown
## 設定
pydantic-settingsを使った`app/config.py`のSettingsクラス:
```python
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
# データベース
DATABASE_URL: str
# 認証
SECRET_KEY: str
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
# アプリ
APP_ENV: str = "development"
DEBUG: bool = False
ALLOWED_ORIGINS: list[str] = ["http://localhost:3000"]
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=True
)
settings = Settings()
インポート方法:
from app.config import settings
ルートハンドラやモデルの中でos.environ.get()を直接使わない。
設定値をハードコードしない。
環境変数は.env(ローカル、gitignore済み)と.env.example(コミット済み)に置く。
## 完全なAGENTS.mdテンプレート
FastAPIプロジェクトにそのまま使えるAGENTS.mdテンプレート。プロジェクト固有のセクションは自分の環境に合わせてカスタマイズする。
```markdown
# AGENTS.md
## プロジェクト概要
FastAPI REST API。Python 3.12。PostgreSQL(asyncpg)。SQLAlchemy 2.x非同期。
マイグレーションはAlembic。JWT認証。テストはpytest with pytest-asyncio。
## コマンド
- 開発サーバー起動: `uvicorn app.main:app --reload`
- テスト実行: `pytest tests/ -v`
- カバレッジ付きテスト: `pytest tests/ --cov=app --cov-report=term-missing`
- マイグレーション作成: `alembic revision --autogenerate -m "description"`
- マイグレーション適用: `alembic upgrade head`
- リント: `ruff check .`
- フォーマット: `ruff format .`
- 型チェック: `mypy app/`
## プロジェクト構造
app/
main.py — FastAPIアプリ、ミドルウェア、startupイベント
config.py — pydantic-settings BaseSettings
dependencies.py — 共有Depends()関数
core/
database.py — エンジン、SessionLocal、Base
security.py — JWT、パスワードハッシュ、OAuth2スキーム
models/ — SQLAlchemy ORMモデルのみ
schemas/ — Pydanticリクエスト/レスポンススキーマのみ
crud/ — DBオペレーション(非同期、引数にdbセッション)
api/
v1/
router.py — include_router()呼び出し
endpoints/ — ルートハンドラモジュール
## 非同期ルール
- すべてのルートハンドラ: `async def`
- すべてのCRUD関数: `async def`
- DBドライバーは非同期(asyncpg)。非同期ハンドラ内で同期DBコールを使わない。
- `BackgroundTasks`はfire-and-forget(メール、ログ)にのみ使用する。
## 依存性注入
- ルートハンドラ内でDBセッションやサービスを直接インスタンス化しない。
- DBセッションには`Depends(get_db)`を使う。
- 認証済みルートには`Depends(get_current_user)`を使う。
- 新しい共有依存関係は`app/dependencies.py`に追加する。
## モデルとスキーマ
- `app/models/` — `Base`を継承するSQLAlchemy ORMモデル。Pydanticをインポートしない。
- `app/schemas/` — APIバリデーション用Pydanticモデル。SQLAlchemyをインポートしない。
- スキーマ命名規則: `UserCreate`、`UserPublic`、`UserUpdate`、`UserInDB`。
- `UserInDB`はORM変換に`model_config = ConfigDict(from_attributes=True)`を使用。
## ルートハンドラ
- Pydanticスキーマインスタンスを返す(FastAPIが`response_model`でシリアライズ)。
- クライアントエラーには`status.HTTP_*`定数を使った`HTTPException`を使う。
- 生のPython例外をraiseしない。
- ルートハンドラにDBクエリを書かない——常に`crud/`に委譲する。
## 認証
- OAuth2スキーム: `OAuth2PasswordBearer(tokenUrl="/api/v1/auth/token")`。
- 保護されたルート: `Depends(get_current_user)`。
- インラインでトークン検証を再実装しない。
## テスト
- テストクライアント: `ASGITransport`を使った`httpx.AsyncClient`。
- 非同期エンドポイントにStarletteの`TestClient`を使わない。
- テストDB: aiosqlite使用のSQLite(インメモリ)。
- フィクスチャでは`app.dependency_overrides`で`get_db`依存関係をオーバーライドする。
- すべてのテスト関数: `@pytest.mark.asyncio`で装飾した`async def`。
## 設定
- 設定: `from app.config import settings`。
- ハンドラやモデルで直接`os.environ.get()`を使わない。
- 環境変数: `.env`(ローカル、gitignore済み)、`.env.example`(プレースホルダー付きでコミット済み)。
## マイグレーション
- 既存のAlembicマイグレーションは絶対に修正しない。
- モデル変更後: `alembic revision --autogenerate -m "description"`、確認してから適用。
## やってはいけないこと
- DBが非同期なのに同期ルートハンドラを使う。
- ルートハンドラに直接DBクエリを書く。
- `app/models/`にPydanticスキーマや`app/schemas/`にORMモデルを置く。
- `app/core/security.py`に存在する認証ロジックを再実装する。
- `app/config.py`外でハードコードした設定値や`os.environ`を呼ぶ。
- テストで`AsyncClient`の代わりに`TestClient`を使う。
完全なCLAUDE.mdテンプレート
# CLAUDE.md
## Claude固有の振る舞い
新しいエンドポイントを追加するように依頼されたら、このシーケンスに従い、
曖昧な点があれば各ステップ後に確認する:
1. ORMモデル(新しいテーブルが必要な場合)
2. Pydanticスキーマ
3. CRUD関数
4. ルートハンドラ
5. テスト
## auth/に触る前に
`app/core/security.py`や`get_current_user`依存関係を変更する前に必ず確認する。
認証のバグはサイレントで発見しにくい。
## マイグレーションのレビュー
`--autogenerate`でAlembicマイグレーションを生成した後は、適用する前に
必ずマイグレーションファイルの内容を見せてほしい。自動生成マイグレーションは
カラム制約を見逃したり、誤った操作を生成することがある。
## テストカバレッジ
新しいルートを追加する時は必ず対応するテストも追加する。TODOコメントを
残すのではなく、テストを書く。
## レスポンススタイル
非同期/同期のトレードオフや依存性注入チェーンを説明する時は具体的に。
「非同期を使う」では不十分——このコードベースで特定の選択がなぜ重要かを教える。
## 便利なコンテキスト
- JWTシークレットは`.env`の`SECRET_KEY`にある。絶対に出力しない。
- テストDBはインメモリSQLite、本番はPostgreSQL。テストと本番で挙動が
違う場合は、コードにPostgreSQL固有のSQLがないか確認する。
- `async_sessionmaker`の`expire_on_commit=False`は意図的なもの——
非同期コンテキストでコミット後のレイジーロードを回避するためだ。
各AIツールのファイル読み込み方法
各ツールがこれらのファイルをどのように処理するかを理解すると、より効果的に記述できる。
Claude Codeはプロジェクトを開くと自動的にAGENTS.mdを読む。ディレクトリツリーを上方向にたどり、見つかったすべてのAGENTS.mdファイルを収集し、ルートから末端の順にマージする。CLAUDE.mdはAGENTS.mdに加えて読まれ、Claude固有の振る舞いを上書きする。AGENTS.mdの指示はチャットレスポンスだけでなく、コード生成そのものを形作る——AsyncClientを使うように書けば、実際に生成するコードでAsyncClientが使われる。
Cursorはデフォルトで.cursorrulesを読むが、「Include AGENTS.md」設定でAGENTS.mdもサポートする。FastAPIプロジェクトでは、AGENTS.mdを正規のソースとして維持し、Cursorの設定を直接使うか.cursorrules → AGENTS.mdのシンボリックリンクを作成するのが確実だ。
GitHub Copilot(VS Code)は.github/copilot-instructions.mdを読む。AGENTS.mdはネイティブには読まれない。チームでCopilotを使うなら、必須の規約を.github/copilot-instructions.mdにコピーするか、複数エージェントを使うならその旨を両ファイルに書いておく。
OpenAI CodexはAGENTS.mdをネイティブに読み、Claude Codeと同じ階層的な解決に従う。上のテンプレートはそのままCodexでも動作する。
複数のAIツールを使うチームへの現実的な提案:AGENTS.mdを唯一の正規ソースとして維持し、ツール固有のファイル(.cursorrules、.github/copilot-instructions.md)をAGENTS.mdの内容を要約するか参照するシンラッパーとして使う。
まとめ
AIエージェントが最も多く間違えるFastAPIパターン——非同期/同期の混在、依存性注入の迂回、スキーマ/モデルの混合、間違いのテストクライアント——はすべて、よく書かれたAGENTS.mdで解決できる。ファイルは網羅的である必要はない。プロジェクト固有の決定事項で、汎用的なPython知識で作業するエージェントが間違えるものをカバーすればいい。
上の完全なテンプレートから始め、自分の環境に当てはまらないセクションを削除し(SQLAlchemyとPydanticを別々に使う代わりにSQLModelを使っているならmodels/schemasセクションを更新する)、同じ間違いをエージェントが繰り返すたびにプロジェクト固有のノートを追加していく。
目標は、AIが生成したコードをフレームワーク上の違反のレビューに費やす時間を減らし、ロジックのレビューにもっと時間をかけることだ。FastAPIの規約をエンコードしたAGENTS.mdはそれを実現してくれる。
他のフレームワークやツール向けのさらなるインストラクションファイルパターンはルールコレクションで見つけられる。
FAQ
FastAPIプロジェクトではAGENTS.mdとCLAUDE.mdのどちらを使うべき?
技術的で規範的なルール(プロジェクト構造・非同期規約・依存性注入・Pydanticスキーマ)はAGENTS.mdに書く。Claude Code・Codex・Cursorなど仕様対応エージェント全てが読むからだ。CLAUDE.mdはスラッシュコマンドなどClaude Code固有の挙動を上乗せする場所で、両者は競合ではなく補完関係にある。
なぜAIエージェントはFastAPIのコードを間違える?
FastAPIは非同期ファーストのハンドラ、境界ごとのPydanticバリデーション、Depends()による依存性注入など、パターンを正確に守ることで真価を発揮する。パターンを知らないエージェントは汎用Python規約にフォールバックし、動くけれどプロジェクトに合わないコードを生成する。
非同期ルートでブロッキング呼び出しを書かせないためには?
AGENTS.mdに非同期ルールを明示する。DB層が非同期かどうか、使うクライアントライブラリ(requestsではなくhttpx.AsyncClient)、async defハンドラ内での同期I/O禁止、の3点だ。非同期ルート内の同期DB呼び出しはテストを通過して本番の負荷で壊れる——最も価値の高いルールと言える。
AGENTS.mdには何を書くべき?
効果が大きいのは、プロジェクト構造とルーター構成、APIバージョニング、依存性注入パターン、非同期ルール、Pydanticスキーマ規約、データベース層、テスト規約、認証、エラーハンドリング、pydantic-settingsによる設定管理。上の完全テンプレートが全てをカバーしている。
PydanticスキーマとORMモデルは同じクラスにすべき?
分けるべきだ。リクエスト/レスポンススキーマはAPI境界のバリデーション、ORMモデルはデータベース状態の表現という別の役割を持つ。エージェントは特にSQLModel使用時に混同しがちなので、AGENTS.mdで2層の分離を明記する。
関連記事
- Claude Code × Python プロジェクト: CLAUDE.md テンプレートと AGENTS.md パターン
- AGENTS.md vs CLAUDE.md どっちを使う?5つの違いと正解(2026年版)
- ユースケース別 AGENTS.md・CLAUDE.md・.cursorrules テンプレート集
APIキーと接続情報をリポジトリの外に置く
FastAPIプロジェクトの.envファイルは、gitignoreしていても各開発者のマシンとCIランナーに本物のデータベースパスワードとAPIキーを平文で置くことになる。ローカルの実験を超えるプロジェクトなら、シークレットはボールトに置いてランタイムで注入するのが安全だ。1Password CLIのop runはアプリの実行中だけ環境変数にシークレットをロードするため、pydantic-settingsは同じように読めるのに、ディスク上には何も残らない。