Files
claudekit/skills/backend-frameworks/references/django.md
T
2026-04-19 14:10:38 +07:00

23 KiB

Backend Frameworks — Django Patterns

Django

When to Use

  • Python web applications
  • Admin interfaces
  • Django REST Framework APIs
  • Content-heavy sites with ORM-driven data models

When NOT to Use

  • FastAPI projects — use the fastapi skill instead for async APIs and microservices
  • JavaScript/Node.js backends (Express, NestJS) — this skill is Python-only
  • Microservices architectures — consider FastAPI instead for lightweight, async services

Core Patterns

1. Models & ORM

Field types and relationships

from django.db import models
from django.utils import timezone

class Organization(models.Model):
    name = models.CharField(max_length=200)
    slug = models.SlugField(unique=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ["name"]

    def __str__(self):
        return self.name

class User(models.Model):
    class Role(models.TextChoices):
        ADMIN = "admin", "Administrator"
        MEMBER = "member", "Member"
        VIEWER = "viewer", "Viewer"

    email = models.EmailField(unique=True)
    name = models.CharField(max_length=100)
    organization = models.ForeignKey(
        Organization,
        on_delete=models.CASCADE,
        related_name="members",
    )
    role = models.CharField(max_length=20, choices=Role.choices, default=Role.MEMBER)
    is_active = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-created_at"]
        indexes = [
            models.Index(fields=["email"]),
            models.Index(fields=["organization", "role"]),
        ]
        constraints = [
            models.UniqueConstraint(
                fields=["organization", "email"],
                name="unique_org_email",
            ),
        ]

    def __str__(self):
        return self.email

class Tag(models.Model):
    name = models.CharField(max_length=50, unique=True)

class Project(models.Model):
    title = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name="owned_projects")
    organization = models.ForeignKey(Organization, on_delete=models.CASCADE)
    tags = models.ManyToManyField(Tag, blank=True, related_name="projects")
    # OneToOneField for 1:1 relationships
    settings = models.OneToOneField(
        "ProjectSettings", on_delete=models.CASCADE, null=True, blank=True
    )

class ProjectSettings(models.Model):
    is_public = models.BooleanField(default=False)
    max_members = models.IntegerField(default=10)

Custom managers and QuerySet methods

class ActiveManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(is_active=True)

class UserQuerySet(models.QuerySet):
    def admins(self):
        return self.filter(role=User.Role.ADMIN)

    def in_organization(self, org_id):
        return self.filter(organization_id=org_id)

    def with_project_count(self):
        return self.annotate(project_count=models.Count("owned_projects"))

class User(models.Model):
    # ... fields ...
    objects = UserQuerySet.as_manager()
    active = ActiveManager()

F objects, Q objects, and annotations

from django.db.models import F, Q, Count, Avg, Sum, Value, When, Case

# F objects: reference model fields in queries
Project.objects.filter(updated_at__gt=F("created_at"))
User.objects.update(login_count=F("login_count") + 1)  # Atomic increment

# Q objects: complex lookups with OR, AND, NOT
User.objects.filter(
    Q(role="admin") | Q(role="member"),
    ~Q(is_active=False),  # NOT inactive
)

# Annotations and aggregations
orgs = Organization.objects.annotate(
    member_count=Count("members"),
    admin_count=Count("members", filter=Q(members__role="admin")),
    avg_projects=Avg("members__owned_projects"),
).filter(member_count__gte=5)

# Conditional expressions
users = User.objects.annotate(
    tier=Case(
        When(owned_projects__count__gte=10, then=Value("power")),
        When(owned_projects__count__gte=3, then=Value("active")),
        default=Value("starter"),
    )
)

# Subqueries
from django.db.models import Subquery, OuterRef
latest_project = Project.objects.filter(
    owner=OuterRef("pk")
).order_by("-created_at").values("title")[:1]
users = User.objects.annotate(latest_project_title=Subquery(latest_project))

2. Views

Function-based views

from django.shortcuts import render, get_object_or_404, redirect
from django.http import JsonResponse
from django.contrib.auth.decorators import login_required

@login_required
def project_detail(request, project_id):
    project = get_object_or_404(
        Project.objects.select_related("owner", "organization"),
        pk=project_id,
    )
    if request.method == "POST":
        form = ProjectForm(request.POST, instance=project)
        if form.is_valid():
            form.save()
            return redirect("project-detail", project_id=project.id)
    else:
        form = ProjectForm(instance=project)

    return render(request, "projects/detail.html", {
        "project": project,
        "form": form,
    })

Class-based views

from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.urls import reverse_lazy

class ProjectListView(LoginRequiredMixin, ListView):
    model = Project
    template_name = "projects/list.html"
    context_object_name = "projects"
    paginate_by = 20

    def get_queryset(self):
        qs = super().get_queryset().select_related("owner", "organization")
        search = self.request.GET.get("q")
        if search:
            qs = qs.filter(
                Q(title__icontains=search) | Q(description__icontains=search)
            )
        return qs

class ProjectCreateView(LoginRequiredMixin, CreateView):
    model = Project
    form_class = ProjectForm
    template_name = "projects/form.html"
    success_url = reverse_lazy("project-list")

    def form_valid(self, form):
        form.instance.owner = self.request.user
        form.instance.organization = self.request.user.organization
        return super().form_valid(form)

class ProjectDeleteView(PermissionRequiredMixin, DeleteView):
    model = Project
    permission_required = "projects.delete_project"
    success_url = reverse_lazy("project-list")

Mixins for reuse

class OrganizationFilterMixin:
    """Filter queryset to the current user's organization."""
    def get_queryset(self):
        return super().get_queryset().filter(
            organization=self.request.user.organization
        )

class ProjectListView(LoginRequiredMixin, OrganizationFilterMixin, ListView):
    model = Project
    # queryset is automatically filtered by organization

API views with Django REST Framework

from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status

@api_view(["GET", "POST"])
@permission_classes([IsAuthenticated])
def project_list(request):
    if request.method == "GET":
        projects = Project.objects.filter(organization=request.user.organization)
        serializer = ProjectSerializer(projects, many=True)
        return Response(serializer.data)

    serializer = ProjectCreateSerializer(data=request.data)
    serializer.is_valid(raise_exception=True)
    serializer.save(owner=request.user)
    return Response(serializer.data, status=status.HTTP_201_CREATED)

3. Migrations

Creating and running migrations

# Generate migrations after model changes
python manage.py makemigrations app_name

# Preview SQL without applying
python manage.py sqlmigrate app_name 0001

# Apply migrations
python manage.py migrate

# Show migration status
python manage.py showmigrations

Data migrations with RunPython

from django.db import migrations

def populate_slugs(apps, schema_editor):
    Organization = apps.get_model("myapp", "Organization")
    from django.utils.text import slugify
    for org in Organization.objects.filter(slug=""):
        org.slug = slugify(org.name)
        org.save(update_fields=["slug"])

def reverse_populate_slugs(apps, schema_editor):
    pass  # No-op reverse

class Migration(migrations.Migration):
    dependencies = [
        ("myapp", "0005_add_slug_field"),
    ]
    operations = [
        migrations.RunPython(populate_slugs, reverse_populate_slugs),
    ]

Squashing migrations

# Squash migrations 0001 through 0010 into one
python manage.py squashmigrations app_name 0001 0010

Tips:

  • Always provide a reverse function for RunPython (even if it is a no-op)
  • Use apps.get_model() in data migrations, never import models directly
  • Test migrations on a copy of production data before deploying

4. Forms

ModelForm with custom validation

from django import forms
from django.core.exceptions import ValidationError

class ProjectForm(forms.ModelForm):
    class Meta:
        model = Project
        fields = ["title", "description", "tags"]
        widgets = {
            "description": forms.Textarea(attrs={"rows": 4}),
            "tags": forms.CheckboxSelectMultiple(),
        }

    def clean_title(self):
        title = self.cleaned_data["title"]
        if "test" in title.lower() and not self.instance.pk:
            raise ValidationError("Title cannot contain 'test' for new projects.")
        return title

    def clean(self):
        cleaned = super().clean()
        title = cleaned.get("title", "")
        description = cleaned.get("description", "")
        if len(title) + len(description) < 20:
            raise ValidationError("Title + description must be at least 20 characters.")
        return cleaned

Formsets

from django.forms import inlineformset_factory

TaskFormSet = inlineformset_factory(
    Project,
    Task,
    fields=["title", "assigned_to", "due_date"],
    extra=2,       # Number of empty forms
    can_delete=True,
    max_num=20,
)

# In a view
def project_tasks(request, project_id):
    project = get_object_or_404(Project, pk=project_id)
    if request.method == "POST":
        formset = TaskFormSet(request.POST, instance=project)
        if formset.is_valid():
            formset.save()
            return redirect("project-detail", project_id=project.id)
    else:
        formset = TaskFormSet(instance=project)
    return render(request, "projects/tasks.html", {"formset": formset})

5. Signals

from django.db.models.signals import post_save, pre_save, m2m_changed
from django.dispatch import receiver

@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    if created:
        UserProfile.objects.create(user=instance)

@receiver(pre_save, sender=Project)
def set_project_slug(sender, instance, **kwargs):
    if not instance.slug:
        from django.utils.text import slugify
        instance.slug = slugify(instance.title)

# Custom signals
from django.dispatch import Signal

project_published = Signal()  # Accepts sender

@receiver(project_published)
def notify_members(sender, project, **kwargs):
    for member in project.organization.members.all():
        send_notification(member, f"Project '{project.title}' published")

# Firing a custom signal
project_published.send(sender=Project, project=project)

When to use signals vs overriding save():

  • Use signals when the action is a side effect (notifications, logging, cache invalidation)
  • Override save() when the logic is core to the model's behavior (setting computed fields)

6. Middleware

import time
from django.utils.deprecation import MiddlewareMixin

class TimingMiddleware(MiddlewareMixin):
    def process_request(self, request):
        request._start_time = time.perf_counter()

    def process_response(self, request, response):
        if hasattr(request, "_start_time"):
            duration = time.perf_counter() - request._start_time
            response["X-Process-Time"] = f"{duration:.4f}"
        return response

# New-style middleware (function-based)
def organization_middleware(get_response):
    def middleware(request):
        if request.user.is_authenticated:
            request.organization = request.user.organization
        else:
            request.organization = None
        response = get_response(request)
        return response
    return middleware

# Register in settings.py
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "myapp.middleware.organization_middleware",  # Custom
    "myapp.middleware.TimingMiddleware",         # Custom
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

7. Django REST Framework

Serializers

from rest_framework import serializers

class UserSerializer(serializers.ModelSerializer):
    project_count = serializers.IntegerField(read_only=True)
    full_name = serializers.SerializerMethodField()

    class Meta:
        model = User
        fields = ["id", "email", "name", "role", "full_name", "project_count", "created_at"]
        read_only_fields = ["id", "created_at"]

    def get_full_name(self, obj):
        return f"{obj.name} ({obj.role})"

class ProjectSerializer(serializers.ModelSerializer):
    owner = UserSerializer(read_only=True)
    tags = serializers.SlugRelatedField(
        many=True, slug_field="name", queryset=Tag.objects.all()
    )

    class Meta:
        model = Project
        fields = ["id", "title", "description", "owner", "tags", "created_at"]

    def validate_title(self, value):
        if len(value) < 3:
            raise serializers.ValidationError("Title must be at least 3 characters.")
        return value

class ProjectCreateSerializer(serializers.ModelSerializer):
    class Meta:
        model = Project
        fields = ["title", "description", "tags"]

ViewSets and routers

from rest_framework import viewsets, permissions, filters
from rest_framework.decorators import action
from rest_framework.response import Response
from django_filters.rest_framework import DjangoFilterBackend

class ProjectViewSet(viewsets.ModelViewSet):
    serializer_class = ProjectSerializer
    permission_classes = [permissions.IsAuthenticated]
    filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
    filterset_fields = ["owner", "tags"]
    search_fields = ["title", "description"]
    ordering_fields = ["created_at", "title"]
    ordering = ["-created_at"]

    def get_queryset(self):
        return Project.objects.filter(
            organization=self.request.user.organization
        ).select_related("owner").prefetch_related("tags")

    def get_serializer_class(self):
        if self.action == "create":
            return ProjectCreateSerializer
        return ProjectSerializer

    def perform_create(self, serializer):
        serializer.save(
            owner=self.request.user,
            organization=self.request.user.organization,
        )

    @action(detail=True, methods=["post"])
    def publish(self, request, pk=None):
        project = self.get_object()
        project.is_published = True
        project.save(update_fields=["is_published"])
        return Response({"status": "published"})

# urls.py
from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register("projects", ProjectViewSet, basename="project")
router.register("users", UserViewSet, basename="user")

urlpatterns = [
    path("api/", include(router.urls)),
]

Permissions

from rest_framework.permissions import BasePermission

class IsOrganizationAdmin(BasePermission):
    def has_permission(self, request, view):
        return (
            request.user.is_authenticated
            and request.user.role == User.Role.ADMIN
        )

class IsOwnerOrReadOnly(BasePermission):
    def has_object_permission(self, request, view, obj):
        if request.method in permissions.SAFE_METHODS:
            return True
        return obj.owner == request.user

Pagination

from rest_framework.pagination import PageNumberPagination, CursorPagination

class StandardPagination(PageNumberPagination):
    page_size = 20
    page_size_query_param = "page_size"
    max_page_size = 100

class TimelinePagination(CursorPagination):
    page_size = 50
    ordering = "-created_at"

# settings.py
REST_FRAMEWORK = {
    "DEFAULT_PAGINATION_CLASS": "myapp.pagination.StandardPagination",
    "PAGE_SIZE": 20,
    "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework_simplejwt.authentication.JWTAuthentication",
    ],
}

8. Admin

from django.contrib import admin
from django.utils.html import format_html

class TaskInline(admin.TabularInline):
    model = Task
    extra = 0
    fields = ["title", "assigned_to", "status", "due_date"]
    readonly_fields = ["created_at"]

@admin.register(Project)
class ProjectAdmin(admin.ModelAdmin):
    list_display = ["title", "owner_name", "organization", "tag_list", "created_at"]
    list_filter = ["organization", "tags", "created_at"]
    search_fields = ["title", "description", "owner__email"]
    readonly_fields = ["created_at", "updated_at"]
    autocomplete_fields = ["owner", "organization"]
    prepopulated_fields = {"slug": ("title",)}
    date_hierarchy = "created_at"
    inlines = [TaskInline]

    fieldsets = (
        (None, {
            "fields": ("title", "slug", "description"),
        }),
        ("Ownership", {
            "fields": ("owner", "organization", "tags"),
        }),
        ("Metadata", {
            "classes": ("collapse",),
            "fields": ("created_at", "updated_at"),
        }),
    )

    def owner_name(self, obj):
        return obj.owner.name
    owner_name.short_description = "Owner"
    owner_name.admin_order_field = "owner__name"

    def tag_list(self, obj):
        return ", ".join(t.name for t in obj.tags.all())
    tag_list.short_description = "Tags"

    def get_queryset(self, request):
        return super().get_queryset(request).select_related(
            "owner", "organization"
        ).prefetch_related("tags")

    # Custom admin actions
    @admin.action(description="Mark selected projects as published")
    def make_published(self, request, queryset):
        count = queryset.update(is_published=True)
        self.message_user(request, f"{count} projects published.")

    actions = [make_published]

@admin.register(User)
class UserAdmin(admin.ModelAdmin):
    list_display = ["email", "name", "organization", "role", "is_active"]
    list_filter = ["role", "is_active", "organization"]
    search_fields = ["email", "name"]
    list_editable = ["role", "is_active"]
    list_per_page = 50

Best Practices

  1. Use select_related and prefetch_related on every query that touches relationsselect_related for ForeignKey/OneToOne (SQL JOIN), prefetch_related for ManyToMany and reverse ForeignKey (separate query). Check queries with django-debug-toolbar.

  2. Keep business logic in model methods or service functions, not in views — views should handle HTTP, forms should handle validation, models/services should handle domain logic. This makes code testable without needing HTTP.

  3. Use get_queryset() for dynamic filtering instead of hardcoding querysets — both in views and DRF ViewSets. This enables mixin composition and per-request filtering (e.g., by organization).

  4. Write data migrations for schema changes that require backfills — never assume fields can be added as non-nullable without a migration to populate existing rows. Use RunPython with a reverse function.

  5. Configure Django REST Framework defaults in settings — set DEFAULT_PAGINATION_CLASS, DEFAULT_PERMISSION_CLASSES, DEFAULT_AUTHENTICATION_CLASSES in REST_FRAMEWORK dict to avoid repeating yourself on each ViewSet.

  6. Use TextChoices / IntegerChoices for enum fields — they integrate with admin filters, serializer validation, and migrations automatically. Avoid plain strings or integers for status/role fields.

  7. Index frequently queried fields — add db_index=True on individual fields or use Meta.indexes for composite indexes. Add UniqueConstraint for business-rule uniqueness.

  8. Use Django's transaction.atomic() for multi-step writes — wrap create/update sequences that must succeed or fail together. DRF's perform_create and perform_update are good places for this.

from django.db import transaction

@transaction.atomic
def transfer_project(project, new_owner):
    old_owner = project.owner
    project.owner = new_owner
    project.save(update_fields=["owner"])
    AuditLog.objects.create(
        action="transfer",
        project=project,
        from_user=old_owner,
        to_user=new_owner,
    )

Common Pitfalls

  1. N+1 queries — accessing project.owner.name in a loop without select_related("owner") fires one query per iteration. Use django-debug-toolbar or nplusone to detect these. Always optimize queryset in get_queryset().

  2. Importing models directly in data migrations — models change over time, but migrations are frozen. Always use apps.get_model("app_name", "ModelName") inside RunPython functions, never from myapp.models import Model.

  3. Forgetting to call full_clean() in model saves — Django's save() does NOT run validators by default. Only forms and serializers call full_clean(). If you save models directly, add explicit validation.

  4. Circular imports between apps — referencing models across apps can cause import cycles. Use string references in ForeignKey: models.ForeignKey("other_app.ModelName", ...) instead of importing the class.

  5. Overusing signals for core logic — signals make code harder to trace and debug. Use them for side effects (sending emails, cache invalidation), not for core domain logic. If logic should always run on save, override save() instead.

  6. Returning entire QuerySets from service functions — QuerySets are lazy, which is usually good, but returning them from service layers can lead to unexpected queries executing in templates. Use .values(), .values_list(), or serialize to dicts when crossing layer boundaries.


  • python — Python language patterns and best practices
  • postgresql — Database integration and query optimization
  • pytest — Testing Django applications with pytest-django