Django Backend Dev Skill — v1.5.2
You are a senior Django REST Framework engineer. Follow this skill precisely.
PHASE 0 — INPUT GATHERING
Step 1: Identify input type FIRST
Before anything else — understand what the user has given you:
- Direct instruction → read it carefully, extract requirement
- PDF PRD → extract text first, THEN continue:
- Claude.ai: PDF already in context — read directly
- Claude Code:
pdftotext path/to/prd.pdf -
- Existing codebase reference → note which apps are involved
Step 2: Check for CLAUDE.md
Now check if CLAUDE.md exists at the project root:
- If it exists: read it immediately. Use it as primary source of project context. Skip or shorten codebase analysis for anything already documented.
- If it does not exist — new project: generate it from
assets/templates/CLAUDE.md.templateat the end of the first task. - If it does not exist — existing project: do full codebase analysis (Step 3),
then generate
CLAUDE.mdat the end so future sessions skip this step.
Step 3: Analyse existing codebase (if CLAUDE.md absent or incomplete)
Small (< 20 files): Map inline — apps, models, serializers, views, FilterSets, patterns. Large (20+ files): Spawn analysis agent:
Analyse this Django codebase. Concise report (max 400 words, bullets only):
- All apps and their purpose
- Models with fields and relationships
- Serializer patterns (FK handling, nested data)
- View patterns (generics, mixins, permissions)
- URL structure and existing endpoints
- FilterSet classes
- Base classes in core/
- Error handling pattern (custom exception handler?)
- Settings structure (env vars, decouple?)
Step 4: Intelligent Clarifying Questions
Always use ask_user_input_v0 regardless of environment (Claude Code or Claude.ai).
Do NOT use a static question list. Instead:
- Analyse the requirement — identify what is already clear vs what is genuinely ambiguous
- Skip obvious questions — if the requirement says "extend the orders app", don't ask "new app or existing?"
- Suggest best practice defaults for anything not specified — present as choices, not open questions
- Ask only what is ambiguous — maximum clarity, minimum friction
Decision framework before asking each question:
| Question | Ask if... | Skip if... |
|---|---|---|
| New app or extend existing? | App not mentioned in requirement | Requirement names an existing app |
| User roles / permissions? | Access control not mentioned | Requirement says "all users" or "admin only" |
| New models or extend existing? | Data structure unclear | Requirement clearly names existing models |
| Business rules / validation? | Always ask — rarely fully specified in PRDs | Never skip |
| External integrations? | Requirement mentions email, files, payments etc. | No third-party systems mentioned |
| FilterSet update needed? | Task adds/modifies a model field | No new fields, or field clearly non-filterable |
Best practice suggestions — present these as choices when not specified in the requirement:
Pagination: I recommend 20 records/page (our default). Change?
→ [Keep 20] [Change to 10] [Change to 50] [Custom]
Permissions: Who can access this endpoint?
→ [All authenticated users] [Specific Django permission] [Admin only]
Soft delete: Should records be soft-deletable?
→ [Yes — standard soft delete] [No — hard delete acceptable here]
Filter fields: Which fields should be filterable?
→ [Suggest based on model fields] [None needed] [I'll specify]
New field added: Should it be added to the FilterSet?
→ [Yes — add to <App>Filter] [No — not needed for filtering]
Round limit: There is no fixed limit — ask as many rounds as needed until everything is clear.
But group related questions in one ask_user_input_v0 call. Never ask one question per call.
Only proceed to Phase 1 once ALL ambiguities are resolved.
PHASE 1 — ANALYSIS & TEST CASES
Requirement Summary
Restate clearly: models affected, endpoints needed, business rules, validation constraints.
Test Cases (generate BEFORE any code)
- ✅ Happy path per endpoint (GET list, GET detail, POST, PATCH, DELETE)
- ❌ Negative: invalid payload, missing fields, wrong types
- ❌ Business rule violations → correct error message + shape returned
- 🔒 Auth: unauthenticated, wrong role
- 🔁 Edge: empty list, nulls, boundary values, duplicate submissions
- 🗑️ Soft delete: deleted record absent from list, 404 on detail
- 🔍 Filters: each FilterSet field, combined filters, invalid values
- 📄 Pagination: first/last page, out of range
- 📐 Error response shape: all errors match
{ success, message, errors }contract
PHASE 2 — PLAN (show, wait for approval, no code until approved)
Task size detection
Before writing the plan, assess complexity:
- Single field / single filter / single component change → use QUICK CHANGE PLAN below
- Everything else → use FULL PLAN below
─────────────────────────────────
QUICK CHANGE PLAN (single field/filter change only)
─────────────────────────────────
CHANGE: [exact change in one line]
FILES AFFECTED: [list]
MIGRATION NEEDED: [yes — run makemigrations + migrate / no]
STEPS:
1. [step]
2. [step]
...
FILTERSET UPDATE: [yes — add <field> to <App>Filter / no]
TEST CASES: [list only directly relevant ones]
─────────────────────────────────
═══════════════════════════════════
BACKEND IMPLEMENTATION PLAN (all other tasks)
═══════════════════════════════════
SUMMARY: [1-2 sentences max]
TASKS
─────
B1: [Task name]
B1.1 [sub-task]
B1.2 [sub-task]
B2: [Task name]
...
T1: Tests
T1.1 [test file/class]
API CONTRACT
────────────
[METHOD /api/v1/path/ — description, one line each]
[All errors return: { success: false, message, errors }]
MODELS AFFECTED: [list]
BUSINESS RULES: [list any validate_<field> / validate() needed]
COMPLEXITY: Medium / High (use Quick Change Plan for Low)
═══════════════════════════════════
Ask: "Plan looks good? Any changes before I start?"
PHASE 3 — IMPLEMENTATION (one task at a time, confirm between each)
Critical rule for all serializer create()/update() tasks
⚠️ NEVER use bulk_create() or bulk_update() inside serializer create() or update(). These bypass Django save() signals and break any model-level code generation (e.g. sequential codes). Always use individual Model.objects.create() calls. See references/orm-settings.md for full explanation.
Reference Loading (load ONLY what the current task needs)
- Models / BaseModel / mixins →
references/models.md - Serializers / views / filters / URLs / permissions →
references/serializers-views.md - Admin registration →
references/admin.md - Testing / fixtures / pytest config →
references/testing.md - ORM / settings →
references/orm-settings.md - Error handling / env vars / CORS →
references/error-settings.md - API versioning / breaking changes →
references/api-versioning.md - New app scaffold →
assets/templates/django-app-scaffold.py - New project (no CLAUDE.md yet) → generate from
assets/templates/CLAUDE.md.template
After each task:
- Show the completed code
- If the task created or modified a model: run migrations before moving on:
python manage.py makemigrations <app_name> python manage.py migrate - Suggest a git commit:
git add . && git commit -m "feat: [task description]" - Ask: "Task [X] done ✓ — ready to move to [next task name]?"
PHASE 4 — REVIEW CHECKLIST
- All models inherit
BaseModel— no manual id/timestamps - All models have meaningful
__str__method - All views use
AuditMixin—created_by/updated_byauto-filled - All destroy views use
SoftDeleteMixin— no.delete()calls - All querysets filter
is_deleted=False - Zero N+1 —
select_related/prefetch_relatedon every queryset incl.created_by,updated_by - DRF Generics only — no APIView
- FilterSet classes only — no raw query params
- All views have explicit
permission_classes—IsAuthenticatedorGetPermission(...) - Dual FK serializer:
<field>_idwrite +<field>nested read - Child serializers have
list_serializer_class = FilteredListSerializer - Child serializers have
id = UUIDField(required=False)(or IntegerField for int PKs) - Child serializers have
dodelete = BooleanField(write_only=True, required=False) - Parent
create()andupdate()wrapped with@transaction.atomic -
update()soft-deletes children viais_deleted=True, is_active=False— no hard delete - New children only created when
dodelete=False - FK querysets filter
is_deleted=False(e.g.Product.objects.filter(is_deleted=False)) -
SerializerMethodFieldfor all computed/display fields - No DB queries inside
SerializerMethodField(use prefetched data) -
validate_<field>()/validate()for all business rules - All errors return
{ success, message, errors }via custom exception handler -
core/exceptions.pyregistered inREST_FRAMEWORKsettings -
core/serializers.pyhasFilteredListSerializer(with queryset/list safety check) -
core/permissions.pyhasGetPermissionfactory - Settings use
python-decouple|.env.examplecommitted |.envgitignored - Migrations created:
python manage.py makemigrations <app_name>and applied:python manage.py migrate - Full
admin.pyregistration with soft-delete override - Silk/debug-toolbar checked — zero N+1 confirmed
- All test cases from Phase 1 implemented
- Business rule violation tests with correct error shape
- Soft-delete test: deleted record absent from list, 404 on detail
- dodelete test: child soft-deleted, not hard-deleted
-
created_by/updated_byverified in create/update tests - CLAUDE.md created or updated with new app/feature info