CLAUDE.md for Django and FastAPI: 13 Rules That Make AI Write Production-Ready Python Web Code
Python web frameworks are where AI assistance has the highest ceiling — and the highest failure rate.
Django and FastAPI have strong conventions. Django has decades of established patterns: models, views, URLs, migrations, signals, the admin, the ORM. FastAPI has its own idioms: Pydantic schemas, dependency injection, async handlers, response models.
Without a CLAUDE.md, the model mixes them. It writes Django ORM queries in FastAPI handlers. It skips Pydantic validation. It forgets select_related on every queryset that touches a foreign key. It generates synchronous code in async functions.
These 13 rules separate AI-generated Python web code that works in development from code that holds up in production.
Rule 1: Pin the framework and version explicitly
Framework: Django 5.x OR FastAPI 0.111+
Python: 3.12+
Do NOT mix Django ORM patterns into FastAPI or vice versa.
This sounds obvious — until you see AI generate from django.db import models in a FastAPI project because both codebases appeared in the same context window.
The version pin matters too. Django 5.x dropped META.db_table_comment compatibility with older migrations. FastAPI 0.100+ changed how Annotated dependencies work. Pin the version and the model respects the API surface that exists.
Rule 2: Django — always use select_related and prefetch_related
Django ORM:
- ForeignKey access in loops: always select_related in the queryset.
- ManyToMany and reverse FK: always prefetch_related.
- Never access related objects without explicit prefetch in views or serializers.
- Log slow queries in development: LOGGING config with django.db.backends DEBUG.
The N+1 query problem is the most common AI-generated Django bug. It doesn't fail. It doesn't raise an error. It just makes your view do 50 database queries instead of 2, and you discover it in production under load.
# AI without rule — N+1 queries
def order_list(request):
orders = Order.objects.all()
for order in orders:
print(order.user.email) # Query per order
# With rule — 2 queries total
def order_list(request):
orders = Order.objects.select_related('user').all()
for order in orders:
print(order.user.email)
Rule 3: FastAPI — every endpoint has a response model
FastAPI:
- Every endpoint declares response_model explicitly.
- Input validated via Pydantic BaseModel — never dict, never raw request.body().
- response_model excludes sensitive fields (passwords, tokens, internal IDs).
- Use Annotated[type, Field(...)] for all schema fields with validation rules.
Without response_model, FastAPI serializes whatever you return. That means internal fields, database IDs, hashed passwords — whatever is in the ORM object — can leak into the response if you're not careful.
# Without rule — leaks internal fields
@app.get("/users/{id}")
async def get_user(id: int, db: Session = Depends(get_db)):
return db.query(User).filter(User.id == id).first() # Returns everything
# With rule — explicit response shape
class UserResponse(BaseModel):
id: int
email: str
display_name: str
model_config = ConfigDict(from_attributes=True)
@app.get("/users/{id}", response_model=UserResponse)
async def get_user(id: int, db: AsyncSession = Depends(get_db)):
user = await db.get(User, id)
if not user:
raise HTTPException(status_code=404)
return user
Rule 4: Django — keep business logic out of views and models
Django architecture:
- Views: HTTP in/out only. No business logic.
- Models: data structure + simple properties. No complex business logic.
- Business logic: service layer in services.py or a services/ package.
- Signals: use sparingly. Prefer explicit service calls over implicit side effects.
AI will put everything in views because that's what tutorials do. A service layer makes logic testable without HTTP context and avoids the "fat model" problem where models become 500-line classes.
# AI without rule — logic in view
def checkout(request):
cart = Cart.objects.get(user=request.user)
for item in cart.items.all():
item.product.stock -= item.quantity
item.product.save()
order = Order.objects.create(user=request.user, total=cart.total)
cart.delete()
send_confirmation_email(order)
return redirect('order_confirm', pk=order.pk)
# With rule — logic in service
# services/checkout.py
def complete_checkout(user: User) -> Order:
cart = Cart.objects.select_related().prefetch_related('items__product').get(user=user)
order = _create_order_from_cart(cart)
_decrement_stock(cart)
cart.delete()
return order
Rule 5: FastAPI — async all the way, or sync all the way
FastAPI async rules:
- Async endpoints: use async def + await everywhere in the call chain.
- Sync endpoints: use def (FastAPI runs these in threadpool automatically).
- Never call sync I/O (requests.get, time.sleep) inside async def.
- Database: use async SQLAlchemy (AsyncSession) in async endpoints.
- Never mix: no asyncio.run() inside a running event loop.
AI generates async functions that call synchronous blocking operations. This blocks the entire event loop and defeats FastAPI's concurrency model.
# Blocks the event loop — silent performance killer
@app.get("/data")
async def get_data():
response = requests.get("https://api.example.com/data") # Sync!
return response.json()
# Correct — async HTTP
@app.get("/data")
async def get_data(client: httpx.AsyncClient = Depends(get_client)):
response = await client.get("https://api.example.com/data")
return response.json()
Rule 6: Django — migrations are sacred
Django migrations:
- Never edit migration files manually after they're committed.
- Never delete migrations. Squash instead of delete.
- Data migrations: use RunPython with atomic=False for large tables.
- Always test migrations forward AND backward (migrate + migrate <prev>).
- Add db_index=True explicitly — don't rely on ORM to decide.
AI will suggest editing migration files to "fix" them. This breaks the migration graph for everyone else on the team and in production. Squash is the correct tool.
Rule 7: FastAPI — dependency injection for everything shared
FastAPI dependencies:
- Database sessions: Depends(get_db) — never instantiate sessions in endpoints.
- Auth context: Depends(get_current_user) — never parse JWT in endpoint body.
- Config/settings: Depends(get_settings) — never import settings directly.
- HTTP clients: Depends(get_http_client) — lifespan-managed, reused across requests.
Dependency injection in FastAPI is not optional style — it's how you get proper lifecycle management, testability, and connection pooling.
# Without rule — session created per query, not per request
@app.get("/items")
async def list_items():
async with AsyncSessionLocal() as db: # New session per endpoint, leaks on exception
return await db.execute(select(Item))
# With rule — session managed by dependency
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with AsyncSessionLocal() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
@app.get("/items")
async def list_items(db: AsyncSession = Depends(get_db)):
result = await db.execute(select(Item))
return result.scalars().all()
Rule 8: Django — use class-based views for CRUD, function views for custom logic
Django views:
- CRUD: use class-based views (ListView, DetailView, CreateView, UpdateView, DeleteView).
- DRF: use ViewSets for standard REST resources.
- Custom business logic: use function-based views.
- Never duplicate URL patterns — use routers for DRF ViewSets.
AI defaults to function-based views for everything because they're simpler to write inline. Class-based views eliminate boilerplate for standard CRUD and make permission checks consistent.
Rule 9: Environment config — never hardcode, always Pydantic Settings
Configuration:
- All config from environment variables.
- FastAPI: pydantic-settings BaseSettings with .env support.
- Django: django-environ or direct os.environ with explicit validation.
- No SECRET_KEY, DATABASE_URL, API keys in source code.
- Fail fast on missing config: raise at import time, not at request time.
# FastAPI — settings validated at startup
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str
secret_key: str
debug: bool = False
allowed_hosts: list[str] = []
model_config = SettingsConfigDict(env_file=".env")
@lru_cache
def get_settings() -> Settings:
return Settings()
Rule 10: Authentication — middleware not manual checks
Auth rules:
- Django: use django-allauth or dj-rest-auth. Never roll custom auth from scratch.
- FastAPI: JWT via python-jose or authlib. Auth in Depends(), not in endpoint body.
- Permission checks: Django permissions system or FastAPI dependency decorators.
- Never check request.user in service layer — pass user as explicit parameter.
AI generates per-endpoint if not request.user.is_authenticated: return 401. This pattern misses endpoints, creates inconsistency, and doesn't compose. Middleware and dependencies enforce auth uniformly.
Rule 11: Error handling — typed exceptions, not bare HTTP responses
Error handling:
- Django: custom exception handler in DRF settings. Typed exception classes.
- FastAPI: @app.exception_handler for custom exceptions. Never return dict with 'error' key manually.
- Never catch Exception broadly without logging.
- All 4xx errors: include error code + human message + field if validation error.
# FastAPI typed error
class ResourceNotFound(Exception):
def __init__(self, resource: str, id: int):
self.resource = resource
self.id = id
@app.exception_handler(ResourceNotFound)
async def not_found_handler(request: Request, exc: ResourceNotFound):
return JSONResponse(
status_code=404,
content={"error": "not_found", "resource": exc.resource, "id": exc.id}
)
Rule 12: Tests — pytest with factories, not fixtures
Testing:
- pytest + pytest-django (Django) or pytest-asyncio (FastAPI).
- Factory Boy for test data — never hardcode test objects.
- Database: use pytest-django's @pytest.mark.django_db. FastAPI: override get_db dependency.
- No real HTTP calls in unit tests — use respx (async) or responses (sync).
- Coverage target: 80% minimum on service layer.
AI generates tests with hardcoded data and repeated setup. Factory Boy + fixtures compose cleanly and scale to complex test scenarios.
Rule 13: The CLAUDE.md block
## Web Framework
**Django 5.x** OR **FastAPI 0.111+** (specify which — do not mix patterns)
**Python:** 3.12+
### Django rules
- select_related/prefetch_related on every queryset touching FK/M2M
- Business logic in services/, not views or models
- Migrations: never edit committed files, squash to fix
- CBVs for CRUD, FBVs for custom logic
### FastAPI rules
- Every endpoint: response_model declared, Pydantic input schema
- Async endpoints: async all the way (no sync I/O inside async def)
- All shared resources via Depends() — db session, auth, settings, HTTP client
- Typed exception handlers, not manual JSONResponse returns
### Both
- Config via environment variables + pydantic-settings / django-environ
- Auth via middleware/dependencies — never manual per-endpoint checks
- pytest + Factory Boy — no hardcoded test data
- No secrets in source code
Why framework-specific rules matter
General Python rules help. Framework-specific rules are what actually prevent the class of bugs that show up in production.
Django's N+1 problem is invisible in tests and fatal under load. FastAPI's sync-in-async mistake is invisible in development and performance-destroying at scale. These patterns don't fail loudly — they degrade silently.
A CLAUDE.md that specifies the framework, the ORM discipline, the async contract, and the architecture layer boundaries turns AI from a fast code generator into a fast code generator that follows the same standards your team already agreed on.
The 15 minutes to write the config pays back on the first PR review where you don't have to add "use select_related here" as a comment.
Top comments (0)