Claude Code with FastAPI: Async Python Workflows
Why FastAPI developers need a different Claude Code setup
Claude Code understands FastAPI. It knows APIRouter, Depends, BackgroundTasks, HTTPException, Pydantic models, and the async request lifecycle. What it does not know is your project: which dependency functions are shared across routers, which Pydantic models are internal versus response-facing, how your async database session is managed, and what your test client setup looks like.
The gap between "Claude knows FastAPI" and "Claude writes FastAPI code that fits your codebase" is entirely closed by configuration. Without a project-specific CLAUDE.md, Claude generates endpoint handlers with inline database calls instead of using your dependency injection layer, Pydantic models without your field validation conventions, and sync tests against async routes.
FastAPI's async-first design adds one more complexity layer. Claude Code will write working async code, but it will mix async def and sync def in ways that create blocking calls inside the event loop, miss await on coroutines in edge cases, and generate test patterns that do not work with pytest-asyncio without extra fixtures.
This guide covers the configuration that removes those failure modes. If you have not set up Claude Code yet, the Claude Code setup guide covers installation and authentication before any of this applies.
The FastAPI CLAUDE.md
The CLAUDE.md at your project root is read before every Claude Code session. For FastAPI, it needs to answer seven questions: how is the project structured, which async database library is in use, how are dependencies organized, how are Pydantic models structured, how are tests run, what is the async session pattern, and what are the hard rules?
# FastAPI project rules
## Project structure
- Entry point: `main.py` (app factory, router registration, lifespan handler)
- Routers: `routers/{resource}.py`, one file per resource group
- Dependencies: `dependencies/` at project root, grouped by concern
- `dependencies/auth.py`, JWT verification, current_user
- `dependencies/database.py`, async session, transaction context
- `dependencies/pagination.py`, shared query params
- Schemas: `schemas/{resource}.py`, Pydantic v2 models
- Base, Create, Update, and Response model variants per resource
- Models: `models/{resource}.py`, SQLAlchemy 2.x ORM models
- Services: `services/{resource}.py`, async business logic, no HTTP concerns
- CRUD: `crud/{resource}.py`, async DB layer called by services
## Python and dependencies
- Python: 3.12
- FastAPI: 0.111+
- Pydantic: v2 only. No Pydantic v1 syntax (no `class Config`, use `model_config` instead)
- SQLAlchemy: 2.x async (AsyncSession, AsyncEngine)
- Database driver: asyncpg (PostgreSQL)
- Auth: python-jose for JWT, passlib for hashing
## Async rules
- All endpoint handlers are async def. No sync def endpoints.
- All database access uses await. Never call ORM methods without await.
- Background tasks: use FastAPI BackgroundTasks or Celery. Never asyncio.create_task in handlers.
- Do not use time.sleep() anywhere. Use asyncio.sleep() when needed.
- Avoid blocking calls (requests.get, open()) inside async functions. Use httpx.AsyncClient and aiofiles.
## Running the project
- Dev: `uvicorn main:app --reload --port 8000`
- Never use `python main.py` to start, always uvicorn
## Tests
- Framework: pytest with pytest-asyncio
- Mode: asyncio_mode = "auto" in pytest.ini
- Test client: httpx.AsyncClient (not TestClient, see below)
- Run: `pytest` from project root
- Fast pass: `pytest tests/routers/` for endpoint tests
- Database: separate test DB (TEST_DATABASE_URL in .env.test)
- Factories: factory_boy with async session support
## Hard rules
- Dependency injection for ALL shared concerns. No global state.
- Services never import from routers. Routers import from services.
- Pydantic schemas never import from SQLAlchemy models.
- Every endpoint has a response_model defined.
- No print() in production code. Use logging.getLogger(__name__).
- No bare except. Catch specific exception types.
Three sections in this CLAUDE.md prevent the most common Claude Code failures with FastAPI.
The async rules section is the most important. Claude Code will write async functions correctly in isolation. The failure modes come from mixing: writing a sync database call inside an async handler, using requests instead of httpx, or calling asyncio.create_task in a context where the task outlives the request. The explicit rules stop all three patterns before they appear.
The Pydantic v2 declaration matters because Claude's training includes significant Pydantic v1 code. Without the explicit version pin, Claude will generate class Config inner classes, @validator decorators, and .dict() method calls, all of which are v1 syntax. Pydantic v2 uses model_config, @field_validator, and .model_dump(). One line in CLAUDE.md locks Claude to the right syntax.
The dependency injection rule prevents inline database calls in handlers. FastAPI's DI system exists precisely so that database connections, authentication, and pagination are not reimplemented per-endpoint. Claude will respect this rule when it is explicit, but will default to inline session creation in handlers otherwise.
Dependency injection patterns Claude Code handles correctly
FastAPI's Depends() system is where Claude Code produces its best output when given clear patterns. The key is defining your dependency functions in CLAUDE.md by example, not just by rule.
Add a dependency examples section to your CLAUDE.md:
## Dependency injection examples
### Async database session
# In dependencies/database.py:
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with AsyncSessionLocal() as session:
async with session.begin():
yield session
# In router:
@router.get("/items")
async def list_items(db: AsyncSession = Depends(get_db)):
...
### Current user from JWT
# In dependencies/auth.py:
async def get_current_user(
token: str = Depends(oauth2_scheme),
db: AsyncSession = Depends(get_db)
) -> User:
...
# Compose in endpoint:
@router.get("/me")
async def get_me(current_user: User = Depends(get_current_user)):
...
### Shared pagination params
# In dependencies/pagination.py:
class PaginationParams:
def __init__(self, skip: int = 0, limit: int = Query(default=20, le=100)):
self.skip = skip
self.limit = limit
With these examples in CLAUDE.md, Claude Code generates dependency chains that match your actual structure rather than inventing new patterns per task. The composition of get_current_user depending on get_db is particularly important to make explicit, because Claude will sometimes generate separate database connections per dependency without guidance.
The async generator pattern for database sessions (async with session.begin(): yield session) is critical for transaction management. Without it in CLAUDE.md, Claude might generate session patterns that leave transactions open or that do not roll back on exception.
Pydantic v2 model conventions
FastAPI's Pydantic integration is the second area where Claude Code needs explicit patterns. The Pydantic v2 migration introduced breaking changes that affect schema design, validation, and serialization.
Add a schemas section to your CLAUDE.md:
## Pydantic v2 schema conventions
### Model structure per resource
from pydantic import BaseModel, ConfigDict, field_validator
class ItemBase(BaseModel):
name: str
description: str | None = None
price: float
class ItemCreate(ItemBase):
pass
class ItemUpdate(BaseModel):
# All fields optional for PATCH semantics
name: str | None = None
description: str | None = None
price: float | None = None
class ItemResponse(ItemBase):
id: int
created_at: datetime
model_config = ConfigDict(from_attributes=True)
### Validators: use @field_validator, not @validator
### Serialization: use .model_dump(), not .dict()
### No orm_mode = True, use model_config = ConfigDict(from_attributes=True)
The from_attributes=True configuration is the most commonly missed Pydantic v2 change. It replaces orm_mode = True from v1. Without it, FastAPI cannot serialize SQLAlchemy model instances into Pydantic response models. Claude Code will generate v1 syntax here if not told otherwise.
The Base/Create/Update/Response split is also worth making explicit. Claude knows this pattern but will invent variations if you do not specify it. Consistent model naming across your codebase means Claude can infer the right schema to use in each context without being told each time.
For a broader look at Claude Code's Python-specific patterns and how to use it effectively across Python projects, the Claude Code Python guide covers the general Python workflow that FastAPI builds on.
pytest-asyncio integration
Testing async FastAPI routes requires a different setup from testing sync Python. Claude Code will generate sync TestClient tests by default, which work for sync endpoints but require careful handling for fully async setups. The recommended pattern uses httpx.AsyncClient with pytest-asyncio.
Add to CLAUDE.md:
## Testing async endpoints
### Test client setup (conftest.py)
import pytest
from httpx import AsyncClient, ASGITransport
from main import app
@pytest.fixture
async def client():
async with AsyncClient(
transport=ASGITransport(app=app),
base_url="http://test"
) as ac:
yield ac
### Database override for tests
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from dependencies.database import get_db
TEST_DATABASE_URL = "postgresql+asyncpg://..."
test_engine = create_async_engine(TEST_DATABASE_URL)
@pytest.fixture
async def db_session():
async with AsyncSession(test_engine) as session:
yield session
@pytest.fixture
async def client_with_db(db_session):
async def override_get_db():
yield db_session
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 example pattern
async def test_create_item(client_with_db: AsyncClient):
response = await client_with_db.post(
"/items",
json={"name": "Test item", "price": 9.99}
)
assert response.status_code == 201
The dependency_overrides pattern is essential for FastAPI testing. It replaces the production database dependency with a test database session at the app level. Claude Code handles this pattern well once it is established in CLAUDE.md, generating correct override teardown with app.dependency_overrides.clear() after each test.
Add to CLAUDE.md:
## After every code change
1. Run `pytest tests/routers/` for endpoint tests (fast)
2. Run `pytest tests/services/` for service layer tests
3. Run `pytest` for full suite before marking any task complete
4. asyncio_mode is "auto", no @pytest.mark.asyncio needed on individual tests
The asyncio_mode = "auto" configuration in pytest.ini means Claude does not need to add @pytest.mark.asyncio decorators to every async test function. Without this in CLAUDE.md, Claude will add the decorator inconsistently, sometimes forgetting it and generating tests that silently never run as coroutines.
The Claude Code testing guide covers the broader test-generation workflow including TDD loops and coverage analysis that apply across Python projects.
Async service layer patterns
FastAPI's performance advantage over sync Python frameworks only holds if the service layer is truly async. Claude Code will write correct async service functions when the pattern is clear, but will fall into two traps without guidance.
Trap one: blocking calls inside async services. Claude knows to use httpx.AsyncClient for HTTP requests, but will sometimes generate requests.get() in service functions when making external API calls in examples or when generating code quickly. Specify in CLAUDE.md:
## External HTTP in services
# Correct: shared async client
from contextlib import asynccontextmanager
@asynccontextmanager
async def get_http_client():
async with httpx.AsyncClient(timeout=30.0) as client:
yield client
# Use in service:
async def fetch_external_data(item_id: int) -> dict:
async with get_http_client() as client:
response = await client.get(f"https://api.example.com/items/{item_id}")
response.raise_for_status()
return response.json()
Trap two: N+1 queries in async ORM usage. SQLAlchemy 2.x async requires explicit relationship loading. Unlike sync SQLAlchemy where lazy loading happens transparently, async SQLAlchemy raises MissingGreenlet errors when you access a lazy-loaded relationship outside the session context. Specify in CLAUDE.md:
## SQLAlchemy 2.x async loading rules
- All relationships used in response models must be explicitly loaded
- Use selectinload() for one-to-many:
result = await db.execute(
select(User).options(selectinload(User.orders)).where(User.id == user_id)
)
- Use joinedload() for many-to-one:
result = await db.execute(
select(Order).options(joinedload(Order.user)).where(Order.id == order_id)
)
- Never access relationship attributes after session context has closed
With these rules in CLAUDE.md, Claude Code generates async service functions that load relationships correctly without you catching the MissingGreenlet error at runtime.
Claude Code permission hooks for FastAPI
FastAPI projects typically have management scripts for database migrations (Alembic), seed data, and environment validation. Claude Code can be configured to gate the dangerous ones behind explicit permission.
In .claude/settings.local.json:
{
"permissions": {
"allow": [
"Bash(uvicorn main:app*)",
"Bash(pytest*)",
"Bash(alembic revision*)",
"Bash(alembic current*)",
"Bash(alembic history*)",
"Bash(alembic check*)"
],
"deny": [
"Bash(alembic upgrade*)",
"Bash(alembic downgrade*)",
"Bash(python -m scripts.drop_db*)",
"Bash(python -m scripts.seed*)"
]
}
}
This setup lets Claude inspect migration state (alembic current, alembic history) and create new revision files (alembic revision), but blocks it from applying or reverting migrations without your explicit command. The seed data block prevents Claude from modifying your development database state as a side effect of a task.
For a complete picture of how Claude Code hooks work across any project type, the Claude Code hooks guide covers the full permission system and what other commands are worth gating.
FastAPI lifespan and startup patterns
FastAPI's lifespan context manager (introduced in v0.93, replacing on_event) is where Claude Code needs explicit guidance. Claude knows both the old @app.on_event("startup") pattern and the new lifespan pattern, and will pick between them based on what it has seen most recently.
Specify in CLAUDE.md:
## App factory and lifespan
# Use lifespan, not @app.on_event:
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup
await init_db()
yield
# Shutdown
await close_db()
app = FastAPI(lifespan=lifespan)
# Router registration after app creation:
app.include_router(items.router, prefix="/items", tags=["items"])
app.include_router(users.router, prefix="/users", tags=["users"])
The lifespan pattern also provides a cleaner way to manage shared resources like connection pools and HTTP clients. Claude Code handles this correctly when the pattern is in CLAUDE.md, generating lifespan handlers that initialize and close resources symmetrically.
What FastAPI developers get wrong first
Three mistakes come up consistently when FastAPI developers start using Claude Code in production projects.
Not specifying asyncpg versus aiosqlite. Claude will generate async SQLAlchemy setup using either driver depending on what is most common in its training data. For PostgreSQL, you want asyncpg. For SQLite in testing, aiosqlite. Without the driver specified in CLAUDE.md, Claude may generate the wrong connection string or missing driver-specific kwargs. One line resolves this: "Database driver: asyncpg. Connection string format: postgresql+asyncpg://user:pass@host/db."
Mixing response models with ORM models. Claude Code will sometimes return SQLAlchemy model instances directly from endpoints instead of serializing through Pydantic response models. FastAPI will attempt to serialize the ORM object, which fails if relationships are lazy-loaded or if the model contains non-serializable fields. The rule is simple: "Every endpoint has a response_model defined. Services return ORM objects; routers serialize them through the response model."
Letting Claude use @app.get on the main app instead of routers. For anything beyond a small prototype, all routes belong on APIRouter instances. Claude will sometimes generate routes directly on app when the task prompt does not mention the router structure. The CLAUDE.md rule: "All routes are defined on APIRouter instances in routers/. The app object in main.py only includes routers."
Getting more from your FastAPI workflow
The CLAUDE.md configuration in this guide produces a FastAPI setup where async patterns are enforced, dependency injection is consistent, Pydantic v2 syntax is used throughout, and pytest-asyncio tests run automatically after every change.
The underlying principle is the same as any Claude Code best practices guide: Claude Code performs at the level of the context you provide. A FastAPI project without CLAUDE.md configuration gets generic async Python code. A project with the configuration above gets endpoint handlers, service functions, and tests that fit your specific codebase without correction.
Start with the CLAUDE.md template, add the async rules and dependency injection examples first, then layer in the Pydantic v2 conventions and Alembic permissions. If you want Claude Code to become the primary author of your FastAPI service layer rather than a review tool, the work of configuring it pays for itself within the first week. Claudify includes a FastAPI-specific CLAUDE.md template as part of the Claude Code workflow kit, pre-configured for async SQLAlchemy, Pydantic v2, and pytest-asyncio setups.
More like this
Ready to upgrade your Claude Code setup?
Get Claudify