Django and Django REST Framework rules for AI coding agents
Django code generated by an agent usually works on a laptop with ten rows and falls over with real data: a template loop that queries per row, a view that fetches by id and checks ownership afterwards, a migration edited after it ran somewhere shared. The Django rules cover models, querysets, migrations, views and DRF serializers on a supported Django release, with or without REST Framework.
They ask the agent to read settings and the installed apps before assuming a feature exists, to give every ForeignKey a deliberate on_delete, and to filter by ownership in get_queryset(). They also note the current limits of the async ORM around transactions and point the agent at python manage.py check --deploy before a release.
Folder: rules/django/ · django.mdc, CLAUDE.md, AGENTS.md
What the Django rules tell your agent
- ModelsEvery
ForeignKeyandOneToOneFieldneeds an expliciton_delete. Choose deliberately:PROTECTfor money and audit data,CASCADEonly for true children. - QuerySetsEvery loop that touches a related object needs
select_related(FK, one-to-one) orprefetch_related(many-to-many, reverse FK). Check withassertNumQueriesor the debug toolbar. - MigrationsNever edit a migration that has been applied anywhere shared. Add a new one.
- Views and DRFFilter by ownership in
get_queryset()(Invoice.objects.filter(account=self.request.user.account)), not after fetching by id. - AsyncAsync views are supported, and the ORM has async methods (
aget,acreate,afirst,async for obj in qs). Through recent releases, transactions do not work in async mode (check the async docs for your version); wrap transactional work in a sync function and call it withsync_to_async.
First rule of each section in django.mdc, quoted as shipped.
Mistakes these rules are written to prevent
- Calling
Model.objects.get()without handlingDoesNotExist. Useget_object_or_404in views. - Signals for core business logic. They hide control flow and are skipped by bulk operations.
- Querying inside a template loop (
{{ order.customer.name }}per row) withoutselect_related.
Source: “Pitfalls to avoid”.
Checks the agent runs before it says done
Example commands from the Django file (swap in your repo's own):
- System check:
python manage.py check - Missing migrations:
python manage.py makemigrations --check --dry-run - Lint:
ruff check . - Tests:
pytest -q(orpython manage.py test)
Run python manage.py check, makemigrations --check, the linter and the test suite, and report each result.
Before and after: Django: hunt N+1 queries and pin the fix with a test
Prompt card 13 · Stack: Django, optionally Django REST Framework · Type: Performance refactor · Best model: a frontier reasoning model
What the card is for
Find the relations touched per row in a view, serializer or template, fix them with select_related, prefetch_related or annotations, and add an assertNumQueries test so the problem cannot return quietly.
Worked example from the card
Before
class OrderSerializer(serializers.ModelSerializer):
customer_name = serializers.CharField(source="customer.name")
item_count = serializers.SerializerMethodField()
class Meta:
model = Order
fields = ["id", "customer_name", "item_count", "total_cents"]
def get_item_count(self, obj):
return obj.items.count()
class OrderViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = OrderSerializer
def get_queryset(self):
return Order.objects.filter(account=self.request.user.account)
Expected AI output (abridged)
Per row: customer.name -> 1 query (FK), items.count() -> 1 COUNT query.
For a page of 50: 1 (count for pagination) + 1 (orders) + 50 + 50 = 102 queries.
Fix: select_related("customer"), annotate(item_count=Count("items")), and read the
annotation in the serializer. Expected: 2 queries (pagination count + orders join).
After
class OrderSerializer(serializers.ModelSerializer):
customer_name = serializers.CharField(source="customer.name")
item_count = serializers.IntegerField(read_only=True)
class Meta:
model = Order
fields = ["id", "customer_name", "item_count", "total_cents"]
class OrderViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = OrderSerializer
def get_queryset(self):
return (Order.objects.filter(account=self.request.user.account)
.select_related("customer")
.annotate(item_count=Count("items"))
.order_by("-created_at"))
@pytest.mark.django_db
def test_order_list_query_count(api_client, user, django_assert_num_queries):
for _ in range(3):
OrderFactory(account=user.account, items=2)
api_client.force_authenticate(user)
with django_assert_num_queries(2): # pagination COUNT + orders with customer join
resp = api_client.get("/api/orders/")
assert resp.status_code == 200
Excerpt from card 13; the prompt itself is in the pack.
More Django prompt cards in the pack
- Card 14: Django: fix lost updates in read-modify-write code
- Card 15: Django: migration safety review for large tables
Using these rules in Cursor, Claude Code and AGENTS.md tools
Cursor
Put _base.mdc and django.mdc in .cursor/rules/. Globs: manage.py, **/*.py, **/templates/**/*.html.
Claude Code
Root CLAUDE.md, base first. Path-scoped variant .claude/rules/django.md for **/*.py, **/templates/**/*.html; PostToolUse hook: ruff format <file>.
AGENTS.md tools (Codex, Gemini CLI, Aider)
Base then stack in one AGENTS.md. Gemini CLI needs context.fileName; Aider needs --read AGENTS.md. Put this file in the directory that holds manage.py. Agents use the AGENTS.md nearest to the file being edited, so a nested copy overrides a root one for that subtree.