DEV Community

Olivia Craft
Olivia Craft

Posted on

CLAUDE.md for Django and FastAPI: 13 Rules That Make AI Write Production-Ready Python Web Code

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.
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode
# 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()
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode
# 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}
    )
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)