Django Framework Guide
Applies to: Django 5+, Django REST Framework, Django Channels Language: Python 3.10+
Core Principles
-
Batteries Included: Leverage Django's built-in features before adding third-party packages
-
DRY: Don't Repeat Yourself -- use abstract models, mixins, and shared utilities
-
Fat Models, Thin Views: Keep business logic in models and services, not views
-
Explicit Over Implicit: Use clear URL patterns, explicit imports, named relationships
-
Security by Default: CSRF, XSS protection, SQL injection prevention are built in
When to Use Django
Good fit:
-
Full-stack web applications with templates
-
Admin interface needed out of the box
-
Content management systems
-
Session-based authentication
-
ORM with built-in migrations
Consider alternatives:
-
Pure APIs with async-first needs (FastAPI)
-
Minimal framework overhead (Flask)
-
Real-time heavy workloads without Channels
Project Structure
myproject/ ├── manage.py ├── pyproject.toml ├── requirements/ │ ├── base.txt │ ├── dev.txt │ └── prod.txt ├── config/ # Project configuration │ ├── init.py │ ├── settings/ │ │ ├── init.py │ │ ├── base.py │ │ ├── dev.py │ │ └── prod.py │ ├── urls.py │ ├── wsgi.py │ └── asgi.py ├── apps/ # Django applications │ ├── users/ │ │ ├── init.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── models.py │ │ ├── views.py │ │ ├── urls.py │ │ ├── forms.py │ │ ├── serializers.py # If using DRF │ │ ├── services.py # Business logic │ │ └── tests/ │ │ ├── init.py │ │ ├── test_models.py │ │ ├── test_views.py │ │ └── test_services.py │ └── core/ # Shared utilities │ ├── init.py │ ├── models.py # Abstract base models │ └── mixins.py ├── templates/ │ ├── base.html │ └── components/ ├── static/ │ ├── css/ │ └── js/ └── tests/ └── conftest.py
-
Split settings into base.py , dev.py , prod.py
-
Group apps under apps/ directory
-
Keep core/ app for shared abstract models and utilities
-
Place business logic in services.py , not in views
-
Co-locate tests inside each app under tests/ directory
Guardrails
Settings
-
Never hardcode SECRET_KEY -- use python-decouple or environment variables
-
Split settings: base.py (shared), dev.py (debug), prod.py (secure)
-
Always define AUTH_USER_MODEL before first migration
-
Set DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
-
Use ALLOWED_HOSTS in production (never ["*"] )
Models
-
Always define str on every model
-
Always set class Meta with db_table , ordering , and indexes
-
Use abstract base models for shared fields (TimeStampedModel , UUIDModel )
-
Use TextChoices /IntegerChoices for status fields (not raw strings)
-
Add related_name to all ForeignKey and OneToOneField relationships
-
Use on_delete explicitly: CASCADE , PROTECT , SET_NULL , SET_DEFAULT
-
Add database indexes for frequently queried fields
-
Use validators at model level for domain constraints
Abstract Base Models
apps/core/models.py
from django.db import models import uuid
class TimeStampedModel(models.Model): """Abstract base with created/updated timestamps.""" created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
class UUIDModel(models.Model): """Abstract base with UUID primary key.""" id = models.UUIDField( primary_key=True, default=uuid.uuid4, editable=False )
class Meta:
abstract = True
Custom User Model
-
Always create a custom user model before the first migration
-
Extend AbstractUser (not AbstractBaseUser unless you need full control)
-
Set AUTH_USER_MODEL = "users.User" in settings
apps/users/models.py
from django.contrib.auth.models import AbstractUser from django.db import models from apps.core.models import TimeStampedModel, UUIDModel
class User(AbstractUser, UUIDModel, TimeStampedModel): email = models.EmailField(unique=True) USERNAME_FIELD = "email" REQUIRED_FIELDS = ["username"]
class Meta:
db_table = "users"
ordering = ["-created_at"]
indexes = [models.Index(fields=["email"])]
def __str__(self) -> str:
return self.email
Views and URLs
Class-Based Views (CBVs)
-
Use ListView , DetailView , CreateView , UpdateView for CRUD
-
Use LoginRequiredMixin for authenticated views
-
Override get_queryset() to add filtering and select_related /prefetch_related
-
Override form_valid() to inject the current user
from django.views.generic import ListView from django.db.models import Q from .models import Product
class ProductListView(ListView): model = Product template_name = "products/list.html" context_object_name = "products" paginate_by = 20
def get_queryset(self):
qs = Product.objects.filter(
status=Product.Status.PUBLISHED
)
search = self.request.GET.get("q")
if search:
qs = qs.filter(
Q(name__icontains=search)
| Q(description__icontains=search)
)
return qs.select_related("category")
URL Configuration
config/urls.py
from django.contrib import admin from django.urls import path, include
urlpatterns = [ path("admin/", admin.site.urls), path("api/", include("apps.api.urls")), path("", include("apps.products.urls")), ]
-
Use include() for app-level URL namespaces
-
Use app_name in each app's urls.py for reverse resolution
-
Serve media files in DEBUG mode only
ORM Essentials
Query Optimization
-
Always use select_related() for ForeignKey/OneToOne (SQL JOIN)
-
Always use prefetch_related() for ManyToMany/reverse FK (separate query)
-
Use only() or defer() to limit fields when not all columns needed
-
Never call Model.objects.all() without pagination or limits
-
Use F() expressions for database-level operations
-
Use Q() objects for complex lookups
Avoiding N+1 Queries
BAD: N+1 queries
for product in Product.objects.all(): print(product.category.name) # Extra query per product
GOOD: Single JOIN query
for product in Product.objects.select_related("category"): print(product.category.name) # No extra queries
Transactions
-
Use @transaction.atomic for multi-step writes
-
Use select_for_update() for optimistic locking
from django.db import transaction
@transaction.atomic def transfer_stock(source_id, dest_id, qty): source = Product.objects.select_for_update().get(id=source_id) dest = Product.objects.select_for_update().get(id=dest_id) source.stock -= qty dest.stock += qty source.save(update_fields=["stock"]) dest.save(update_fields=["stock"])
Admin Configuration
from django.contrib import admin from .models import Product
@admin.register(Product) class ProductAdmin(admin.ModelAdmin): list_display = ["name", "category", "price", "status"] list_filter = ["status", "category", "created_at"] search_fields = ["name", "description"] prepopulated_fields = {"slug": ("name",)} readonly_fields = ["created_at", "updated_at"] ordering = ["-created_at"]
fieldsets = (
(None, {"fields": ("name", "slug", "description")}),
("Pricing", {"fields": ("price", "stock")}),
("Classification", {"fields": ("category", "status")}),
("Timestamps", {
"fields": ("created_at", "updated_at"),
"classes": ("collapse",),
}),
)
-
Always register models with @admin.register(Model)
-
Use list_display for useful columns, list_filter for filtering
-
Use prepopulated_fields for slug generation
-
Group fields with fieldsets for organized admin forms
Django REST Framework (DRF)
Settings
REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": [ "rest_framework.authentication.SessionAuthentication", "rest_framework_simplejwt.authentication.JWTAuthentication", ], "DEFAULT_PERMISSION_CLASSES": [ "rest_framework.permissions.IsAuthenticated", ], "DEFAULT_PAGINATION_CLASS": ( "rest_framework.pagination.PageNumberPagination" ), "PAGE_SIZE": 20, "DEFAULT_THROTTLE_RATES": { "anon": "100/hour", "user": "1000/hour", }, }
ViewSets and Routers
-
Use ModelViewSet for full CRUD
-
Use @action(detail=True/False) for custom endpoints
-
Override get_serializer_class() for different read/write serializers
-
Override get_queryset() to add select_related /prefetch_related
from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.response import Response
class ProductViewSet(viewsets.ModelViewSet): queryset = Product.objects.select_related("category") permission_classes = [IsAuthenticatedOrReadOnly]
def get_serializer_class(self):
if self.action in ["create", "update", "partial_update"]:
return ProductCreateSerializer
return ProductSerializer
def perform_create(self, serializer):
serializer.save(created_by=self.request.user)
@action(detail=True, methods=["post"])
def publish(self, request, pk=None):
product = self.get_object()
product.status = Product.Status.PUBLISHED
product.save(update_fields=["status"])
return Response({"status": "published"})
Serializers
-
Use ModelSerializer for standard CRUD
-
Add read_only_fields for computed/auto fields
-
Validate in validate_<field>() or validate() methods
-
Use nested serializers for read, flat IDs for write
Security Essentials
-
CSRF: Enabled by default for forms; use @csrf_exempt sparingly
-
XSS: Django templates auto-escape by default; never use |safe with user data
-
SQL Injection: ORM uses parameterized queries; never use raw SQL with string formatting
-
Clickjacking: X-Frame-Options middleware enabled by default
-
HTTPS: Set SECURE_SSL_REDIRECT = True in production
-
HSTS: Set SECURE_HSTS_SECONDS in production
-
Cookies: Set SESSION_COOKIE_SECURE = True and CSRF_COOKIE_SECURE = True
-
Passwords: Use AUTH_PASSWORD_VALIDATORS (enabled by default)
-
CORS: Use django-cors-headers with explicit allowed origins (never CORS_ALLOW_ALL_ORIGINS in production)
Testing
Standards
-
Use pytest with pytest-django (not Django's built-in test runner)
-
Mark database tests with @pytest.mark.django_db
-
Use factory-boy or fixtures for test data
-
Use APIClient for DRF endpoint testing
-
Coverage target: >80% for business logic
-
Test file naming: test_models.py , test_views.py , test_services.py
Fixtures
conftest.py
import pytest from rest_framework.test import APIClient from apps.users.models import User
@pytest.fixture def api_client(): return APIClient()
@pytest.fixture def user(db): return User.objects.create_user( username="testuser", email="test@example.com", password="testpass123", )
@pytest.fixture def authenticated_client(api_client, user): api_client.force_authenticate(user=user) return api_client
Commands Reference
Development
python manage.py runserver python manage.py shell_plus # django-extensions
Migrations
python manage.py makemigrations python manage.py migrate python manage.py showmigrations
Testing
pytest pytest -v --cov=apps --cov-report=html pytest apps/products/ -k "test_create"
Database
python manage.py dbshell python manage.py dumpdata products > fixtures/products.json python manage.py loaddata fixtures/products.json
Static files
python manage.py collectstatic
Celery
celery -A config worker -l info celery -A config beat -l info
Dependencies
Base: Django>=5.0 , djangorestframework , django-cors-headers , django-filter , djangorestframework-simplejwt , python-decouple , psycopg2-binary , whitenoise
Dev: pytest , pytest-django , pytest-cov , factory-boy , django-debug-toolbar , django-extensions , black , ruff , mypy , django-stubs
Best Practices
Do
-
Use select_related and prefetch_related for every queryset
-
Create indexes for frequently queried fields
-
Use @transaction.atomic for multi-step operations
-
Validate at both model level and serializer level
-
Write services for business logic (not in views)
-
Use signals sparingly (prefer explicit service calls)
-
Cache expensive queries with Django cache framework
-
Use environment variables for all configuration
Don't
-
Put business logic in views
-
Use raw SQL without parameterization
-
Ignore N+1 query problems
-
Store sensitive data in settings files
-
Use Model.objects.all() without limits
-
Skip migrations in production
-
Use CORS_ALLOW_ALL_ORIGINS = True in production
-
Use |safe template filter with user-provided data
Advanced Topics
For detailed code examples and advanced patterns, see:
- references/patterns.md -- DRF serializers, middleware, services, signals, Celery tasks, management commands, deployment, and testing patterns
External References
-
Django Documentation
-
Django REST Framework
-
Two Scoops of Django
-
Django Best Practices