AGENTS.md FastAPI Python Claude Code async pytest 2026

AGENTS.md for FastAPI: Complete Patterns Guide for AI Coding Agents (2026)

The Prompt Shelf ·

FastAPI is one of the most opinionated Python frameworks — in a good way. It has clear patterns for dependency injection, async route handlers, Pydantic models, and type annotations. Those patterns are also exactly the kind of thing an AI coding agent will ignore or get subtly wrong without explicit instruction.

Out of the box, Claude Code or Cursor working on a FastAPI project will:

  • Mix async def and def route handlers without understanding the performance implications
  • Place Pydantic models and SQLAlchemy models in the same file because nothing told it not to
  • Use TestClient when AsyncClient is needed for async endpoints
  • Generate authentication middleware that doesn’t match your existing OAuth2PasswordBearer setup
  • Create new utility functions instead of wiring through Depends()

An AGENTS.md file in your FastAPI project root fixes all of this. It’s not documentation for humans — it’s a machine-readable contract that shapes how the agent understands your codebase before it writes a single line.

Why FastAPI Specifically Needs AGENTS.md

Most Python web frameworks have loose conventions. FastAPI doesn’t. The framework rewards you for following its patterns precisely: async-first route handlers, Pydantic for validation at every boundary, dependency injection for shared logic, and OpenAPI schema as a first-class output.

When an agent doesn’t know these patterns, it falls back to generic Python conventions. Those aren’t wrong — but they’re not FastAPI. You end up reviewing a lot of code that technically works but doesn’t fit the project.

Three things make FastAPI particularly worth instrumenting:

Async is all-or-nothing in practice. A sync database call inside an async def route blocks the entire event loop. An agent that doesn’t know your DB layer is async (or isn’t) will write blocking code that passes tests and fails in production under load.

Dependency injection is the API. FastAPI’s Depends() system is how the framework handles database sessions, authentication, rate limiting, and shared configuration. Agents that don’t know about your dependency hierarchy will either bypass it with direct instantiation or create parallel implementations that break your middleware.

Pydantic models serve two distinct roles. Your request/response schemas (validation at the API boundary) and your ORM models (database representations) look similar but should never be conflated. Without explicit instruction, agents collapse them — especially with SQLModel, which deliberately blurs the line.

AGENTS.md vs CLAUDE.md for FastAPI Projects

These two files are not interchangeable, and mixing up their purpose is a common mistake.

AGENTS.md is the agentic-context file. It tells AI agents how to operate in your codebase: where files live, what commands to run, what patterns to follow when generating code, and what to avoid. It’s read by Claude Code, OpenAI Codex, Cursor, and any agent that supports the spec. Keep it technical and prescriptive.

CLAUDE.md (Claude Code specific) adds Claude-specific behaviors on top: slash command definitions, memory hints, project-specific personas, and instructions for how Claude should explain things to you. It’s the right place for “when in doubt, ask before touching authentication code” or “this project follows a specific migration naming convention — here’s the pattern.”

For a FastAPI project with a single developer or small team, a single AGENTS.md often covers everything. Add CLAUDE.md when you have Claude-specific workflows that don’t apply to other agents, or when you want to encode your review preferences and communication style.

A reasonable split:

AGENTS.md       → project architecture, patterns, commands, conventions
CLAUDE.md       → workflow preferences, asking behavior, review style, shortcuts

Project Structure Patterns to Encode

The project structure section of your AGENTS.md is load-bearing. FastAPI projects vary more than most frameworks — there’s no Rails-style mandated directory layout — so agents need explicit guidance.

Here’s a structure that scales well:

project/
├── app/
│   ├── main.py              # FastAPI app instantiation, middleware, startup events
│   ├── dependencies.py      # Shared Depends() functions (db session, current user, etc.)
│   ├── config.py            # pydantic-settings BaseSettings class
│   ├── api/
│   │   ├── v1/
│   │   │   ├── router.py    # APIRouter, include_router() calls
│   │   │   └── endpoints/
│   │   │       ├── users.py
│   │   │       └── items.py
│   │   └── v2/              # future version, same structure
│   ├── models/
│   │   └── user.py          # SQLAlchemy ORM models only
│   ├── schemas/
│   │   └── user.py          # Pydantic request/response schemas only
│   ├── crud/
│   │   └── user.py          # Database operations, takes db session as arg
│   └── core/
│       ├── security.py      # JWT creation, password hashing
│       └── database.py      # Engine creation, SessionLocal, Base
├── tests/
│   ├── conftest.py          # pytest fixtures, test db setup
│   └── api/
│       └── v1/
│           └── test_users.py
├── alembic/                 # Database migrations
├── AGENTS.md
└── pyproject.toml

In AGENTS.md, you document this explicitly so the agent never has to guess:

## Project Structure

The project follows a layered architecture. When adding a new resource:

1. `app/models/<resource>.py` — SQLAlchemy ORM model
2. `app/schemas/<resource>.py` — Pydantic request/response schemas
3. `app/crud/<resource>.py` — Database operations (no route logic here)
4. `app/api/v1/endpoints/<resource>.py` — Route handlers (no DB logic here)
5. Register the router in `app/api/v1/router.py`

Do NOT put Pydantic schemas in the models directory or ORM models in schemas.
Do NOT put database queries directly in route handlers — always go through crud/.

Router Organization and Versioning

FastAPI’s APIRouter with versioning is clean but easy to get wrong when an agent adds new endpoints. Your AGENTS.md should make the router inclusion pattern explicit:

## API Versioning

Routes are versioned. New endpoints go in `app/api/v1/endpoints/`.

Router registration in `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"])

In app/main.py:

app.include_router(api_router, prefix="/api/v1")

Result: endpoints are available at /api/v1/users/, /api/v1/items/, etc.

Do not put route handlers directly in main.py.


## Dependency Injection Patterns

This is the section that prevents the most bugs. FastAPI's `Depends()` system is powerful and agents frequently bypass it by creating new instances directly. Your AGENTS.md needs to document the dependency chain explicitly.

```markdown
## Dependency Injection

Core dependencies live in `app/dependencies.py`. Always use `Depends()` — never
instantiate services or create DB sessions directly in route handlers.

### Database Session

```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()

Usage in routes:

@router.get("/users/{user_id}")
async def get_user(
    user_id: int,
    db: AsyncSession = Depends(get_db)
):
    ...

Current User

# 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

When adding authentication to a route, import and use get_current_user — do not re-implement token validation inline.


## Async Patterns

This is where agents make the most consequential mistakes. Document the async conventions explicitly.

```markdown
## Async Rules

**All route handlers must be `async def`.** This project uses an async database
driver (asyncpg / aiosqlite). Sync route handlers block the event loop.

```python
# CORRECT
@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)

# WRONG — do not do this
@router.get("/items/{item_id}")
def get_item(item_id: int, db: AsyncSession = Depends(get_db)):
    ...

CRUD functions must be async:

# 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()

Background tasks for fire-and-forget work: Use BackgroundTasks only for operations that should not block the response (sending email, logging to external services). Do not use background tasks for operations that the caller needs the result of.

@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 Schema Conventions

Pydantic models for FastAPI request/response handling follow specific patterns that agents should know:

```markdown
## Pydantic Schema Conventions

Schemas live in `app/schemas/`. They are separate from ORM models in `app/models/`.

Standard schema pattern for a resource:

```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 — incoming POST body (includes password)
  • UserPublic — outgoing response (never includes password)
  • UserInDB — ORM → Pydantic conversion (uses from_attributes=True)
  • UserUpdate — PATCH body (all fields optional)

Route handlers return Pydantic schemas, not ORM model instances directly:

@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 serializes via response_model

## Database Layer Patterns

Document which ORM you use and how the session is wired:

```markdown
## Database

This project uses SQLAlchemy 2.x with async support (asyncpg driver for PostgreSQL).

Engine and session factory in `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 models extend 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)

Migrations: Alembic. After model changes:

  1. alembic revision --autogenerate -m "description"
  2. Review the generated migration
  3. alembic upgrade head

Never modify existing migrations. Create new ones.


## Testing Conventions

Testing async FastAPI endpoints correctly requires `pytest-asyncio` and `httpx.AsyncClient`. This is one of the most common areas where agents generate code that doesn't match the project setup.

```markdown
## Testing

Test framework: pytest with pytest-asyncio. Test client: httpx AsyncClient
(not Starlette's synchronous TestClient).

### conftest.py structure

```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()

Test pattern

# 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

Run tests: pytest tests/ -v Run with coverage: pytest tests/ --cov=app --cov-report=term-missing

Use AsyncClient — do not use Starlette’s TestClient for async endpoints.


## Authentication Patterns

```markdown
## Authentication

This project uses JWT with OAuth2 password flow. Auth utilities in `app/core/security.py`.

### OAuth2 scheme
```python
# app/core/security.py
from fastapi.security import OAuth2PasswordBearer

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/token")

Token creation

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)

Protected routes

@router.get("/users/me", response_model=UserPublic)
async def get_current_user_profile(
    current_user: User = Depends(get_current_user)
):
    return current_user

Always use Depends(get_current_user) for protected routes. Do not verify tokens manually in route handlers.


## Error Handling

```markdown
## Error Handling

Use `HTTPException` for all client errors. Never raise bare Python exceptions in
route handlers.

Standard patterns:
```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")

Use status.HTTP_* constants from fastapi — do not hardcode integer status codes.

For validation errors, let Pydantic handle them. FastAPI converts RequestValidationError to 422 automatically — do not catch and re-raise validation errors unless you need custom error formatting.

For unexpected server errors, let them propagate. Do not catch generic Exception in route handlers. Add a global exception handler in main.py if you need custom 500 responses.


## Configuration with pydantic-settings

```markdown
## Configuration

Settings class in `app/config.py` using pydantic-settings:

```python
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    # Database
    DATABASE_URL: str

    # Auth
    SECRET_KEY: str
    ACCESS_TOKEN_EXPIRE_MINUTES: int = 30

    # App
    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()

Import settings like this:

from app.config import settings

Never use os.environ.get() directly in route handlers or models. Never hardcode configuration values. Environment variables go in .env (local, gitignored) and .env.example (committed).


One caveat on the `.env` approach: even gitignored, that file holds real database passwords and API keys in plaintext on every developer's machine and CI runner. For anything past a local toy project, keep the actual secrets in a vault and inject them at runtime — [1Password CLI's `op run`](https://1password.partnerlinks.io/z5r1pmkqu36z) loads them into the process environment only while the app runs, so `pydantic-settings` reads them exactly the same way while nothing sensitive sits on disk.

## Complete AGENTS.md Template

Here's a copy-paste-ready AGENTS.md for a FastAPI project. Customize the project-specific sections for your setup.

```markdown
# AGENTS.md

## Project Overview
FastAPI REST API. Python 3.12. PostgreSQL (asyncpg). SQLAlchemy 2.x async.
Alembic for migrations. JWT authentication. pytest with pytest-asyncio for testing.

## Commands
- Start dev server: `uvicorn app.main:app --reload`
- Run tests: `pytest tests/ -v`
- Run tests with coverage: `pytest tests/ --cov=app --cov-report=term-missing`
- Create migration: `alembic revision --autogenerate -m "description"`
- Apply migrations: `alembic upgrade head`
- Lint: `ruff check .`
- Format: `ruff format .`
- Type check: `mypy app/`

## Project Structure
app/
  main.py           — FastAPI app, middleware, startup events
  config.py         — pydantic-settings BaseSettings
  dependencies.py   — Shared Depends() functions
  core/
    database.py     — Engine, SessionLocal, Base
    security.py     — JWT, password hashing, OAuth2 scheme
  models/           — SQLAlchemy ORM models only
  schemas/          — Pydantic request/response schemas only
  crud/             — Database operations (async, takes db session as arg)
  api/
    v1/
      router.py     — include_router() calls
      endpoints/    — Route handler modules

## Async Rules
- All route handlers: `async def`
- All CRUD functions: `async def`
- DB driver is async (asyncpg). Never use sync DB calls in async handlers.
- Use `BackgroundTasks` only for fire-and-forget (email, logging).

## Dependency Injection
- Never instantiate DB sessions or services directly in route handlers.
- Use `Depends(get_db)` for database sessions.
- Use `Depends(get_current_user)` for authenticated routes.
- Add new shared dependencies to `app/dependencies.py`.

## Models vs Schemas
- `app/models/` — SQLAlchemy ORM models extending `Base`. Never import Pydantic here.
- `app/schemas/` — Pydantic models for API validation. Never import SQLAlchemy here.
- Schema naming convention: `UserCreate`, `UserPublic`, `UserUpdate`, `UserInDB`.
- `UserInDB` uses `model_config = ConfigDict(from_attributes=True)` for ORM conversion.

## Route Handlers
- Return Pydantic schema instances (FastAPI serializes via `response_model`).
- Use `HTTPException` with `status.HTTP_*` constants for all client errors.
- Never raise bare Python exceptions.
- No database queries in route handlers — always delegate to `crud/`.

## Authentication
- OAuth2 scheme: `OAuth2PasswordBearer(tokenUrl="/api/v1/auth/token")`.
- Protected routes: `Depends(get_current_user)`.
- Never re-implement token verification inline.

## Testing
- Test client: `httpx.AsyncClient` with `ASGITransport`.
- Do NOT use Starlette `TestClient` for async endpoints.
- Test DB: SQLite with aiosqlite (in-memory).
- Override `get_db` dependency in fixtures via `app.dependency_overrides`.
- All test functions: `async def` decorated with `@pytest.mark.asyncio`.

## Configuration
- Settings: `from app.config import settings`.
- Never use `os.environ.get()` directly in handlers or models.
- Env vars: `.env` (local, gitignored), `.env.example` (committed with placeholders).

## Migrations
- Never modify existing Alembic migrations.
- After model changes: `alembic revision --autogenerate -m "description"`, review, then apply.

## What to Avoid
- Sync route handlers when DB is async.
- Direct DB queries in route handlers.
- Pydantic schemas in `app/models/` or ORM models in `app/schemas/`.
- Re-implementing auth logic that exists in `app/core/security.py`.
- Hardcoded config values or `os.environ` calls outside `app/config.py`.
- Using `TestClient` instead of `AsyncClient` in tests.

Complete CLAUDE.md Template

# CLAUDE.md

## Claude-Specific Behavior

When I ask you to add a new endpoint, follow this sequence and check in with me
after each step if anything is ambiguous:
1. ORM model (if new table is needed)
2. Pydantic schemas
3. CRUD functions
4. Route handler
5. Tests

## Before Touching auth/
Always ask me before modifying anything in `app/core/security.py` or the
`get_current_user` dependency. Auth bugs are silent and hard to catch.

## Migration Review
After generating an Alembic migration with `--autogenerate`, always show me the
migration file content before applying it. Autogenerated migrations sometimes
miss column constraints or produce incorrect operations.

## Test Coverage
When adding new routes, always add corresponding tests. Don't leave me a TODO
comment — write the test.

## Response Style
When explaining async vs sync tradeoffs or dependency injection chains, be specific.
"Use async" is not enough — tell me why a specific choice matters for this codebase.

## Useful Context
- The JWT secret is in `.env` as `SECRET_KEY`. Never echo it.
- The test DB is in-memory SQLite. Production is PostgreSQL. If you see behavior
  differences between test and prod, check for PostgreSQL-specific SQL in the code.
- `expire_on_commit=False` on our `async_sessionmaker` is intentional — it avoids
  lazy-loading after commit in async context.

How Different AI Tools Read These Files

Understanding how each tool processes these files helps you write them more effectively.

Claude Code reads AGENTS.md automatically when you open a project. It walks up the directory tree collecting all AGENTS.md files it encounters, merging them root-to-leaf. CLAUDE.md is read in addition to AGENTS.md and takes precedence for Claude-specific behaviors. Instructions in AGENTS.md shape code generation, not just chat responses — tell Claude to use AsyncClient, and it will use AsyncClient in the code it writes.

Cursor reads .cursorrules by default but also supports AGENTS.md through the “Include AGENTS.md” setting. For FastAPI projects, the most reliable approach is to maintain AGENTS.md as the canonical source and either use Cursor’s AGENTS.md support directly or symlink .cursorrules → AGENTS.md.

GitHub Copilot in VS Code reads .github/copilot-instructions.md. It doesn’t read AGENTS.md natively. If your team uses Copilot, copy the essential conventions into .github/copilot-instructions.md — or maintain both files if you use multiple agents on the project.

OpenAI Codex reads AGENTS.md natively, following the same hierarchical resolution as Claude Code. The template above works for Codex without modification.

For teams using multiple AI tools, the pragmatic approach is: maintain AGENTS.md as the source of truth, then use tool-specific files (.cursorrules, .github/copilot-instructions.md) as thin wrappers that point to or summarize the AGENTS.md content.

Putting It Together

The FastAPI patterns that cause the most AI agent errors — async/sync mixups, dependency injection bypasses, schema/model conflation, wrong test client — are all fixable with a well-written AGENTS.md. The file doesn’t need to be exhaustive. It needs to cover the decisions that are specific to your project and that an agent working from generic Python knowledge would get wrong.

Start with the complete template above, remove sections that don’t apply to your setup (if you’re using SQLModel instead of separate SQLAlchemy + Pydantic, update the models/schemas section accordingly), and add project-specific notes as you catch agents making the same mistakes repeatedly.

The goal is to spend less time reviewing AI-generated code for framework violations and more time reviewing it for logic. An AGENTS.md that encodes your FastAPI conventions gets you there.

You can find more instruction file patterns for other frameworks and tools in our rules collection.

FAQ

Should a FastAPI project use AGENTS.md or CLAUDE.md?

Use AGENTS.md for the technical, prescriptive rules — project structure, async conventions, dependency injection, Pydantic schemas — because it is read by Claude Code, Codex, Cursor, and any agent supporting the spec. CLAUDE.md adds Claude Code-specific behaviors on top. They complement each other rather than compete.

Why do AI coding agents get FastAPI code wrong?

FastAPI rewards following its patterns precisely: async-first handlers, Pydantic validation at every boundary, Depends()-based dependency injection. Agents that don’t know these patterns fall back to generic Python conventions — code that technically works but doesn’t fit the project.

How do I stop an agent from writing blocking calls in async routes?

State your async rules explicitly in AGENTS.md: whether the DB layer is async, which client libraries to use (httpx.AsyncClient, not requests), and that sync I/O inside async def handlers is forbidden. A sync DB call in an async route passes tests but fails in production under load — this is the single highest-value rule to encode.

What should an AGENTS.md for FastAPI include?

The high-leverage sections: project structure and router organization, API versioning, dependency injection patterns, async rules, Pydantic schema conventions, database layer patterns, testing conventions, authentication, error handling, and configuration via pydantic-settings. The complete template above covers all of them.

Should Pydantic schemas and ORM models be the same class?

No. Request/response schemas validate at the API boundary; ORM models represent database state. Agents tend to conflate them — especially with SQLModel — so AGENTS.md should state explicitly that the two layers stay separate.

Related Articles

Explore the collection

Browse all AI coding rules — CLAUDE.md, .cursorrules, AGENTS.md, and more.

Browse Rules