Skill: Django Patterns
Purpose
Guide Claude Code when implementing Django/DRF features. Follow these conventions unless the project's CLAUDE.md overrides them.
Project Structure
- Project settings module is always named
project/ (not the service name)
- Apps live in their own top-level directories
- Each app has:
admin.py, apps.py, models.py, serializers.py, services.py, tasks.py, urls.py, views.py, tests/
- Tests use a
tests/ directory with files per concern: test_models.py, test_views.py, test_services.py
Models
- All models inherit from
AbstractModel (provides uuid, metadata, created_at, updated_at, silent_save())
- Use the inherited
uuid field for public-facing IDs, keep integer PKs internal
- Add
__str__ methods to every model
- Use
Meta.ordering to define default ordering
- Keep models thin — business logic goes in
services.py
Service Layer
- Each app has a
services.py — all business logic lives here, not in views or models
- Services are classes with external dependencies injected via
__init__
- Views call services; services call models and external APIs
- This keeps views thin and business logic testable without HTTP
Serializers (DRF)
- Separate read vs write serializers when shapes differ
- Use
SerializerMethodField sparingly — prefer annotations at the queryset level
- Validate at the serializer level, not the view level
- Always define
fields explicitly (never use __all__)
Views
- Prefer ViewSets for CRUD, APIView for custom actions
- Use
select_related / prefetch_related on querysets to avoid N+1
- Views should only: parse request, call service, return serialized response
- Use
permission_classes explicitly on every view
URLs
- Each app exports
urlpatterns from urls.py
project/urls.py aggregates with namespacing: path("api/v1/{app}/", include(({app}_urlpatterns, "{app}")))
- Use DRF routers for ViewSets
Canonical API Contract
This is the source of truth for the cross-stack API. The Django backend ships exactly this; every decoupled client (Next.js, Flutter) consumes exactly this. Keep paths, identifiers, and payload shapes byte-for-byte consistent with this section — do not restate them differently elsewhere.
Prefix: every endpoint lives under /api/v1 with a trailing slash on every path.
Identifiers: clients only ever see uuid. Models keep an internal integer primary key for FK/index efficiency, but URLs and payloads expose the public uuid field only — never the integer id. Detail routes are keyed on it: path("<uuid:uuid>/", ...).
Auth endpoints:
| Method |
Path |
Request |
Response |
| POST |
/api/v1/auth/login/ |
{email, password} |
{access, refresh} |
| POST |
/api/v1/auth/refresh/ |
{refresh} |
{access} |
| GET |
/api/v1/auth/me/ |
— (Bearer access token) |
user object |
| POST |
/api/v1/auth/register/ |
{username, email, password, first_name, last_name} |
{user, tokens: {access, refresh}} |
- Login returns the token pair directly:
{access, refresh}.
- Refresh returns
{access} (a new access token from a valid refresh token).
- Register wraps the created user and its tokens:
{user, tokens: {access, refresh}}.
- The user object (from
/api/v1/auth/me/ and inside register's user) is {uuid, username, email, first_name, last_name} — no integer id.
Resource endpoints follow REST collection/detail conventions under the prefix, keyed by uuid:
| Method |
Path |
Purpose |
| GET |
/api/v1/{resource}/ |
list (paginated) |
| POST |
/api/v1/{resource}/ |
create |
| GET |
/api/v1/{resource}/<uuid>/ |
retrieve |
| PATCH |
/api/v1/{resource}/<uuid>/ |
update |
| DELETE |
/api/v1/{resource}/<uuid>/ |
delete |
(The self-contained full-stack Next.js app applies the same prefix, uuid, and pagination shape to its own route handlers — e.g. /api/v1/posts — though Next.js route paths omit the trailing slash.)
Pagination: list endpoints are page-number paginated. Query params: page and page_size. Response shape:
{
"page": 1,
"count": 42,
"num_pages": 5,
"results": [ ... ]
}
count is the total item count across all pages, num_pages the total page count, page echoes the current page, and results holds the current page's items. (This is a custom paginator — not DRF's default {count, next, previous, results}.)
Celery Tasks
- Use
@shared_task(bind=True, max_retries=3, default_retry_delay=60)
- Tasks call services — no business logic in the task itself
- Keep tasks in each app's
tasks.py
Migrations
- Review auto-generated migrations before committing
- Add
RunPython data migrations when needed alongside schema changes
- Never edit a migration that's been pushed to main
Testing
- Use
pytest-django with @pytest.mark.django_db
- Use factory_boy for test data (not fixtures)
- Test API endpoints via
APIClient — test the contract, not internals
- Test services independently with unit tests (no HTTP layer)
- Aim for: service logic tests, serializer validation tests, endpoint integration tests
Development Environment
- All commands run via
make (Docker Compose under the hood) — never run python manage.py directly
- Use
/django-bootstrap when setting up a new project from scratch
Integration Summaries
After completing a backend feature, generate an integration summary:
- List all new/modified endpoints with method, path, auth requirements
- Include request/response shapes as JSON examples
- Note any pagination, filtering, or ordering parameters
- Document error response shapes
- Save to
docs/integration/[feature-name].md
1---2name: django-patterns3description: Django/DRF project conventions: service-layer structure, serializers, thin views, URL namespacing, the canonical /api/v1 API contract, Celery tasks, and testing. Apply when writing or reviewing Django/Python backend code.4---56# Skill: Django Patterns78## Purpose9Guide Claude Code when implementing Django/DRF features. Follow these conventions unless the project's CLAUDE.md overrides them.1011## Project Structure12- Project settings module is always named `project/` (not the service name)13- Apps live in their own top-level directories14- Each app has: `admin.py`, `apps.py`, `models.py`, `serializers.py`, `services.py`, `tasks.py`, `urls.py`, `views.py`, `tests/`15- Tests use a `tests/` directory with files per concern: `test_models.py`, `test_views.py`, `test_services.py`1617## Models18- All models inherit from `AbstractModel` (provides `uuid`, `metadata`, `created_at`, `updated_at`, `silent_save()`)19- Use the inherited `uuid` field for public-facing IDs, keep integer PKs internal20- Add `__str__` methods to every model21- Use `Meta.ordering` to define default ordering22- Keep models thin — business logic goes in `services.py`2324## Service Layer25- Each app has a `services.py` — all business logic lives here, not in views or models26- Services are classes with external dependencies injected via `__init__`27- Views call services; services call models and external APIs28- This keeps views thin and business logic testable without HTTP2930## Serializers (DRF)31- Separate read vs write serializers when shapes differ32- Use `SerializerMethodField` sparingly — prefer annotations at the queryset level33- Validate at the serializer level, not the view level34- Always define `fields` explicitly (never use `__all__`)3536## Views37- Prefer ViewSets for CRUD, APIView for custom actions38- Use `select_related` / `prefetch_related` on querysets to avoid N+139- Views should only: parse request, call service, return serialized response40- Use `permission_classes` explicitly on every view4142## URLs43- Each app exports `urlpatterns` from `urls.py`44- `project/urls.py` aggregates with namespacing: `path("api/v1/{app}/", include(({app}_urlpatterns, "{app}")))`45- Use DRF routers for ViewSets4647## Canonical API Contract4849This is the **source of truth** for the cross-stack API. The Django backend ships exactly this; every decoupled client (Next.js, Flutter) consumes exactly this. Keep paths, identifiers, and payload shapes byte-for-byte consistent with this section — do not restate them differently elsewhere.5051**Prefix:** every endpoint lives under `/api/v1` with a trailing slash on every path.5253**Identifiers:** clients only ever see `uuid`. Models keep an internal integer primary key for FK/index efficiency, but URLs and payloads expose the public `uuid` field only — never the integer `id`. Detail routes are keyed on it: `path("<uuid:uuid>/", ...)`.5455**Auth endpoints:**5657| Method | Path | Request | Response |58|--------|------|---------|----------|59| POST | `/api/v1/auth/login/` | `{email, password}` | `{access, refresh}` |60| POST | `/api/v1/auth/refresh/` | `{refresh}` | `{access}` |61| GET | `/api/v1/auth/me/` | — (Bearer access token) | user object |62| POST | `/api/v1/auth/register/` | `{username, email, password, first_name, last_name}` | `{user, tokens: {access, refresh}}` |6364- **Login** returns the token pair directly: `{access, refresh}`.65- **Refresh** returns `{access}` (a new access token from a valid refresh token).66- **Register** wraps the created user and its tokens: `{user, tokens: {access, refresh}}`.67- The **user object** (from `/api/v1/auth/me/` and inside register's `user`) is `{uuid, username, email, first_name, last_name}` — no integer `id`.6869**Resource endpoints** follow REST collection/detail conventions under the prefix, keyed by `uuid`:7071| Method | Path | Purpose |72|--------|------|---------|73| GET | `/api/v1/{resource}/` | list (paginated) |74| POST | `/api/v1/{resource}/` | create |75| GET | `/api/v1/{resource}/<uuid>/` | retrieve |76| PATCH | `/api/v1/{resource}/<uuid>/` | update |77| DELETE | `/api/v1/{resource}/<uuid>/` | delete |7879(The self-contained full-stack Next.js app applies the same prefix, `uuid`, and pagination shape to its own route handlers — e.g. `/api/v1/posts` — though Next.js route paths omit the trailing slash.)8081**Pagination:** list endpoints are page-number paginated. Query params: `page` and `page_size`. Response shape:8283```json84{85 "page": 1,86 "count": 42,87 "num_pages": 5,88 "results": [ ... ]89}90```9192`count` is the total item count across all pages, `num_pages` the total page count, `page` echoes the current page, and `results` holds the current page's items. (This is a custom paginator — not DRF's default `{count, next, previous, results}`.)9394## Celery Tasks95- Use `@shared_task(bind=True, max_retries=3, default_retry_delay=60)`96- Tasks call services — no business logic in the task itself97- Keep tasks in each app's `tasks.py`9899## Migrations100- Review auto-generated migrations before committing101- Add `RunPython` data migrations when needed alongside schema changes102- Never edit a migration that's been pushed to main103104## Testing105- Use `pytest-django` with `@pytest.mark.django_db`106- Use factory_boy for test data (not fixtures)107- Test API endpoints via `APIClient` — test the contract, not internals108- Test services independently with unit tests (no HTTP layer)109- Aim for: service logic tests, serializer validation tests, endpoint integration tests110111## Development Environment112- All commands run via `make` (Docker Compose under the hood) — never run `python manage.py` directly113- Use `/django-bootstrap` when setting up a new project from scratch114115## Integration Summaries116After completing a backend feature, generate an integration summary:117- List all new/modified endpoints with method, path, auth requirements118- Include request/response shapes as JSON examples119- Note any pagination, filtering, or ordering parameters120- Document error response shapes121- Save to `docs/integration/[feature-name].md`