Django Apps skill for Claude Code

Constant query counts and migrations safe on live data. Claude measures queries before touching a Django view, then fixes N+1 with select_related, prefetch_related and annotations, and locks the result in with assertNumQueries. Migrations are read as SQL and planned for live tables, tasks are enqueued in on_commit, and DRF viewsets are scoped per user so list endpoints never leak other customers' rows.

  • namedjango-apps
  • size178 lines · ≈1,950 tokens
  • files1
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Builds and reviews Django 5.x projects - model design, safe migrations, ORM query efficiency with select_related and prefetch_related, transactions and on_commit, Django REST Framework serializers and viewsets, admin performance, and environment-driven settings with deploy checks. Use when writing Django models, views, DRF APIs or migrations, when a Django page or endpoint issues too many queries, or when preparing Django settings for production.

Try asking

  • The orders admin page takes 8 seconds. Find the slow queries
  • Add a NOT NULL column to this 50M-row table without downtime
  • Build a DRF endpoint for invoices scoped to the current user

Use it when

  • Writing or reviewing Django models, views or DRF serializers
  • A Django page or API endpoint issues dozens of queries
  • Shipping migrations against large production tables
  • Hardening Django settings before a production deploy

Skip it when

How to check the result

Tests pass with assertNumQueries guards on touched views, makemigrations --check is clean, sqlmigrate shows no blocking rewrites, and check --deploy reports no warnings.

The common mistake it prevents

Rendering nested DRF serializers without preloading in get_queryset, turning a 50-row page into hundreds of queries.

~/.claude/skills/django-apps/SKILL.md

--- name: django-apps description: Builds and reviews Django 5.x projects - model design, safe migrations, ORM query efficiency with select_related and prefetch_related,… license: MIT ---

A healthy Django codebase keeps query counts constant per request, ships migrations that can run against a live database, puts business rules in models or service functions rather than views, and passes manage.py check --deploy with production settings.

Workflow

- [ ] 1. Check versions (python -m django --version) and settings layout
- [ ] 2. Read the models and the URLs/views touched by the task
- [ ] 3. Measure queries for the affected views before changing them
- [ ] 4. Make the change: models -> migration -> services -> views/serializers
- [ ] 5. Review the migration SQL and its locking behavior
- [ ] 6. Test with assertNumQueries and the test client
- [ ] 7. Run checks: makemigrations --check, check --deploy, tests

Django 5.2 is the current LTS (supported to April 2028); Django 6.0 shipped December 2025. Features worth using when available: db_default and GeneratedField (5.0), CompositePrimaryKey (5.2), async ORM methods (aget, acreate, async for), and the built-in tasks API in 6.0 (which still needs a real worker backend for production).

3. Measure queries first

from django.db import connection, reset_queries
from django.test.utils import CaptureQueriesContext

with CaptureQueriesContext(connection) as ctx:
    response = client.get("/orders/")
print(len(ctx.captured_queries))
for q in ctx.captured_queries[:10]:
    print(q["sql"][:200])

Or install django-debug-toolbar in development and read the SQL panel. Repeated identical queries differing only by ID are N+1.

4. Models and the ORM

class Order(models.Model):
    class Status(models.TextChoices):
        PENDING = "pending"
        PAID = "paid"
        CANCELED = "canceled"

    customer = models.ForeignKey("customers.Customer", on_delete=models.PROTECT, related_name="orders")
    status = models.CharField(max_length=16, choices=Status, default=Status.PENDING, db_index=True)
    total = models.DecimalField(max_digits=12, decimal_places=2)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        indexes = [models.Index(fields=["customer", "-created_at"])]
        constraints = [models.CheckConstraint(condition=models.Q(total__gte=0), name="order_total_gte_0")]

(CheckConstraint(condition=...) replaced check= in Django 5.1.)

Access patternUse
Forward FK / one-to-one used per rowselect_related("customer") (SQL JOIN)
Reverse FK / M2M used per rowprefetch_related("items") (second query)
Prefetch with filter/order/nested selectPrefetch("items", queryset=Item.objects.select_related("product"))
Counts/sums per rowannotate(item_count=Count("items"))
Only a few columns.only(...), .values(...), .values_list(..., flat=True)
Existence check.exists(), never len(qs) or if qs: on large sets
Update without raceF("stock") - 1 in .update()
Large loops.iterator(chunk_size=2000)
Many inserts/updatesbulk_create(batch_size=1000), bulk_update(objs, ["field"])

bulk_create, update() and delete() on querysets skip save() and signals; do not rely on signals for invariants.

Transactions

from django.db import transaction

@transaction.atomic
def place_order(customer, cart):
    order = Order.objects.create(customer=customer, total=cart.total)
    Product.objects.filter(pk__in=cart.ids).select_for_update()   # lock rows in this tx
    ...
    transaction.on_commit(lambda: send_confirmation.delay(order.pk))

Enqueue tasks and send emails in on_commit, never inside the transaction: a worker can otherwise read a row that does not exist yet, or act on a rolled-back order. Keep ATOMIC_REQUESTS off for APIs with external calls; wrap explicit units instead.

5. Migrations

python manage.py makemigrations --check --dry-run     # CI: fail if models drift
python manage.py sqlmigrate orders 0042               # read the SQL before shipping
python manage.py migrate --plan
ChangeSafe approach on a large live table
Add nullable columnSafe
Add column with defaultUse db_default or nullable + backfill; on PostgreSQL 11+ a constant default is metadata-only
Add NOT NULL to existing columnAdd as nullable, backfill in batches, then add constraint in a later deploy
Add index on PostgresAddIndexConcurrently from django.contrib.postgres.operations in a migration with atomic = False
Rename column/modelExpand-contract: add new, dual-write, backfill, switch reads, drop old later
Remove fieldFirst deploy code that no longer reads it; then drop in the next release
Data migrationRunPython(forwards, backwards) using apps.get_model(), batched, never importing models directly

Never edit a migration that has run in any shared environment; add a new one. Squash only with team agreement. Dropping columns or tables requires explicit user confirmation.

DRF

class OrderViewSet(viewsets.ModelViewSet):
    serializer_class = OrderSerializer
    permission_classes = [IsAuthenticated]

    def get_queryset(self):
        return (Order.objects.filter(customer__user=self.request.user)   # object-level scoping
                .select_related("customer")
                .prefetch_related("items__product"))
  • Scope get_queryset() by user or tenant; queryset = Order.objects.all() with only IsAuthenticated exposes every order to every user.
  • Nested serializers cause N+1 unless get_queryset preloads exactly what they read.
  • Use explicit fields = [...], never "__all__" on write serializers (mass assignment).
  • Set DEFAULT_PAGINATION_CLASS (cursor pagination for large tables) and PAGE_SIZE.
  • Put cross-field validation in validate(), side effects in services called from perform_create, not in serializers’ save() overrides scattered across the code.

Admin

list_select_related = True (or a tuple), autocomplete_fields or raw_id_fields for FKs to big tables (the default dropdown loads every row), show_full_result_count = False and list_per_page for huge changelists, readonly_fields for audit columns.

Settings

  • One settings.py reading environment variables (django-environ or os.environ), or base/dev/prod modules; secrets never committed.
  • Production: DEBUG = False, ALLOWED_HOSTS, CSRF_TRUSTED_ORIGINS, SECURE_PROXY_SSL_HEADER when behind a TLS-terminating proxy, SESSION_COOKIE_SECURE, CSRF_COOKIE_SECURE, SECURE_HSTS_SECONDS, CONN_MAX_AGE (or connection pooling via OPTIONS: {"pool": True} with psycopg 3 on Django 5.1+), STORAGES for static/media.
DJANGO_SETTINGS_MODULE=config.settings.prod python manage.py check --deploy

Gotchas

  • default=timezone.now() (called) freezes the value at import; pass the callable.
  • on_delete=CASCADE on a FK to users deletes business records with the user; prefer PROTECT or SET_NULL for financial data.
  • save() overwrites every column; use save(update_fields=[...]) in concurrent code.
  • .count() and len() on the same queryset run separate work; reuse results.
  • Signals hide control flow and do not fire for bulk operations; prefer explicit calls.
  • Async views calling sync ORM methods raise SynchronousOnlyOperation; use async ORM methods or sync_to_async.

Verification

  1. python manage.py test (or pytest --ds=...) passes; views touched have with self.assertNumQueries(N): guards so N+1 regressions fail CI.
  2. makemigrations --check is clean and sqlmigrate output shows no full-table rewrite or blocking index build on large tables.
  3. check --deploy reports no warnings for production settings.
  4. Manually request a list endpoint as a second user: it returns only that user’s rows.

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc