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.
Cross-app logic rule
⚠️ Logic touching models from MORE THAN ONE app → create a service class in services.py. Same-app logic stays in serializer. See references/services.md.
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 setup (pytest.ini, factories, conftest) →
references/testing-setup.md - Serializer + API view tests →
references/testing.md - Advanced tests (services, signals, concurrency) →
references/testing-advanced.md - ORM / settings →
references/orm-settings.md - Error handling / env vars / CORS →
references/error-settings.md - API versioning / breaking changes →
references/api-versioning.md - Cross-app service layer →
references/services.md - Sequential code generation (ORD-0001) →
references/code-generation.md - Django signals (model events → tasks/cache/notifications) →
references/services.md(signals section) - Redis caching → route to
django-integrations-dev→references/caching.md - Audit log (who did what, when, from where) →
references/audit-log.md - Multi-tenancy (shared-schema with tenant_id) →
references/multi-tenancy.md(only if multi-tenant chosen at setup) - Feature flags (custom implementation) →
references/feature-flags.md - Full-text search — PostgreSQL →
references/search-postgres.md(default for <1M records) - Full-text search — Elasticsearch →
references/search-elasticsearch.md(for scale or fuzzy/faceted search) - Field-level encryption (MultiFernet rotating keys) →
references/field-encryption.md(for PII, secrets, tokens) - GDPR compliance (cookie consent + data export) →
references/gdpr-compliance.md(if EU/EEA users) - 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
Adaptive checklist: Skip any item that was explicitly opted out of during Phase 0 clarifying questions (e.g. user chose hard delete → skip SoftDeleteMixin item; user chose no OAuth → skip OAuth items). The checklist reflects defaults — document any deliberate deviations in CLAUDE.md.
- 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 -
throttle_classeson all public-facing and mutation endpoints (rate limiting) -
DEFAULT_THROTTLE_RATESconfigured in settings (anon + user rates) - Integration test: full request → serializer → DB → response cycle verified
- 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
If audit logging is enabled (enterprise):
-
AuditLogmodel registered with GenericForeignKey and DB-level DELETE trigger -
AuditContextMiddlewarein MIDDLEWARE (after AuthenticationMiddleware) - Tracked models use
@track_auditdecorator — auto-log on save/delete - Manual log points added: login, logout, sensitive access, data export
- Audit log API endpoint created (read-only, staff-only permission)
- Test: audit entry created on model save, update captures diff, delete blocked
If multi-tenancy is enabled:
-
Tenantmodel created with subdomain/slug strategy chosen - Business models inherit
TenantAwareBaseModel(NOT plain BaseModel) -
TenantMiddlewareregistered AFTER auth, BEFORE audit middleware - JWT claims include
tenant_id(set in token serializer) - Composite indexes:
(tenant, ...)on every business model - Unique constraints scoped to tenant (e.g.
unique_together=['tenant', 'code']) - Admin uses
.all_tenants()to bypass default filtering - Celery tasks wrapped in
TenantContext(tenant=...)context manager - Test: queryset returns 0 rows when accessed from wrong tenant context
If feature flags are used:
-
FeatureFlagmodel created with OFF/ON/ROLLOUT/TARGETED states -
FeatureFlagsMiddlewareregistered AFTER TenantMiddleware - Flag cache invalidated on
post_saveandm2m_changedsignals - Flag keys follow
<area>.<feature-name>dotted-kebab convention - Views use
feature_flag_requireddecorator orFeatureFlagRequiredmixin (returns 404 not 403) -
last_toggled_by+last_toggled_atpopulated in adminsave_model - Frontend-visible flags exposed via
/api/flagsendpoint with allow-list - Celery tasks use
is_enabled_for_user()helper (no request in worker) - Test: rollout is sticky (same user always same bucket), kill switch wins
If full-text search is enabled (Postgres):
-
SearchableMixinadded to searchable models with declaredsearch_fieldsweights - GIN index on
search_vector; composite(tenant, search_vector)for multi-tenant - Signal OR DB trigger rebuilds
search_vectoron every save - Every search query filters by
tenantFIRST (missing = data leak) -
ts_headlineused for highlighted snippets in API response -
reindex_searchmanagement command available for bulk rebuild - Test: cross-tenant isolation — search in tenant B cannot find tenant A rows
If full-text search is enabled (Elasticsearch):
-
@registry.register_documenton each searchable Document class -
tenant_idindexed as KeywordField on every document - Index name prefixed with
{APP_NAME}_{ENV}_— no cross-env pollution - Analyzer chain:
lowercase_analyzer+ stop/stem;autocomplete_analyzerif used - EVERY query calls
.filter('term', tenant_id=str(tenant.pk))FIRST - Health check endpoint added to monitoring
-
search_index --populatecommand verified in CI/deploy pipeline - Test: cross-tenant isolation — manual assertion since no ORM auto-filter
If field-level encryption is enabled:
-
cryptographylibrary in requirements.txt -
FERNET_KEYSset in .env (comma-separated; position 0 is current) -
EncryptedCharField/EncryptedTextFieldused for all PII/secret fields - Sensitive fields with lookup need: separate
<field>_hashSHA-256 column - Admin
list_displaydoes NOT include encrypted fields (N decryptions/page) -
search_fieldsin admin excludes encrypted fields -
rotate_encryptionmanagement command available for key rotation - Test: DB raw query on encrypted field shows ciphertext, not plaintext
- Test: hash-based lookup finds the row via normalized+hashed value
- ADR in CLAUDE.md §7 documenting: what's encrypted, why, key rotation policy
CLAUDE.md v2 Update Rules (saas-dev 4.0.0+)
At the end of Phase 3, update CLAUDE.md following the v2 protocol. Full rules:
saas-dev/references/router/claude-md-update-protocol.md. Quick reference for this skill:
Always update:
- §2
last_updated— today's date - §3
version_last_used— current saas-dev version - §9 Recent Changes — prepend one entry:
| YYYY-MM-DD | [SKILL_NAME] | [VERSION] | [change] |
Update as relevant to work done:
- §4 Dependency Registry — new packages added (version + one-line purpose)
- §5 Environment Variables — new env vars (under correct subsection)
- §6 Third-Party Integrations — new row if integration added
- §7 Architecture Decisions — new ADR for non-obvious design choices
- §8 Known Issues — append if discovered during work
Emit update checkpoint to chat:
✓ CLAUDE.md updated:
§4: +N dependencies
§5: +N env vars
§7: +ADR-NNN (title)
§9: +1 change entry
Full format spec: saas-dev/references/router/claude-md-v2.md