
Django Expert
- 4.8k installs
- 117 repo stars
- Updated July 23, 2026
- vintasoftware/django-ai-plugins
django-expert is an agent skill for Expert Django backend development guidance. Use when creating Django models, views, serializers, or APIs; debugging ORM
About
The django-expert skill expert Django backend development guidance. Use when creating Django models, views, serializers, or APIs; debugging ORM queries or migrations; optimizing database performance; implementing authentication; writing tests; or working with Django REST Framework. Follows Django best practices and modern patterns.. Django Expert Overview This skill provides expert guidance for Django backend development with comprehensive coverage of models, views, Django REST Framework, forms, authentication, testing, and performance optimization. It follows official Django best practices and modern Python conventions to help you build robust, maintainable applications. Key Capabilities: - Model design with optimal ORM patterns - View implementation (FBV, CBV, DRF viewsets) - Django REST Framework API development - Query optimization and performance tuning - Authentication and permissions - Testing strategies and patterns - Security best practices When to Use Invoke this skill when you encounter these triggers: Model & Database Work: - "Create a Django model for..." - "Optimize this queryset/database query" - "Generate migrations for..." - "Design database schema for..." - "Fix.
- Expert Django backend development guidance. Use when creating Django models, views, serializers, or APIs; debugging ORM
- Model design with optimal ORM patterns
- View implementation (FBV, CBV, DRF viewsets)
- Django REST Framework API development
- Query optimization and performance tuning
Django Expert by the numbers
- 4,826 all-time installs (skills.sh)
- +122 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #127 of 2,203 Security skills by installs in the Skillselion catalog
- Security screen: HIGH risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
django-expert capabilities & compatibility
- Capabilities
- expert django backend development guidance. use · model design with optimal orm patterns · view implementation (fbv, cbv, drf viewsets)
What django-expert says it does
Expert Django backend development guidance. Use when creating Django models, views, serializers, or APIs; debugging ORM queries or migrations; optimizing database performance; impl
npx skills add https://github.com/vintasoftware/django-ai-plugins --skill django-expertAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 4.8k |
|---|---|
| repo stars | ★ 117 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 23, 2026 |
| Repository | vintasoftware/django-ai-plugins ↗ |
How do I run django-expert tasks with correct setup and documented commands?
Expert Django backend development guidance. Use when creating Django models, views, serializers, or APIs; debugging ORM queries or migrations; optimizing database performance; implementing authenticat
Who is it for?
Developers automating django expert via agent-guided SKILL.md workflows.
Skip if: Skip when unrelated tooling already covers the task without this skill's documented flow.
When should I use this skill?
Expert Django backend development guidance. Use when creating Django models, views, serializers, or APIs; debugging ORM queries or migrations; optimizing database performance; implementing authenticat
What you get
Repeatable django-expert workflows with grounded commands and expected outputs.
- secure serializer classes
- DRF view configurations
- API security examples
Files
Django Expert
Overview
This skill provides expert guidance for Django backend development with comprehensive coverage of models, views, Django REST Framework, forms, authentication, testing, and performance optimization. It follows official Django best practices and modern Python conventions to help you build robust, maintainable applications.
Key Capabilities:
- Model design with optimal ORM patterns
- View implementation (FBV, CBV, DRF viewsets)
- Django REST Framework API development
- Query optimization and performance tuning
- Authentication and permissions
- Testing strategies and patterns
- Security best practices
When to Use
Invoke this skill when you encounter these triggers:
Model & Database Work:
- "Create a Django model for..."
- "Optimize this queryset/database query"
- "Generate migrations for..."
- "Design database schema for..."
- "Fix N+1 query problem"
View & API Development:
- "Create an API endpoint for..."
- "Build a Django view that..."
- "Implement DRF serializer/viewset"
- "Add filtering/pagination to API"
Authentication & Security:
- "Implement authentication/permissions"
- "Create custom user model"
- "Secure this endpoint/view"
Testing & Quality:
- "Write tests for this Django app"
- "Debug this Django error/issue"
- "Review Django code for issues"
Performance & Optimization:
- "This Django view is slow"
- "Optimize database queries"
- "Add caching to..."
Production Deployment:
- "Deploy Django to production"
- "Configure Django for production"
- "Set up HTTPS/SSL for Django"
- "Production settings checklist"
- "Configure production database/cache"
Instructions
Follow this workflow when handling Django development requests:
1. Analyze the Request and Gather Context
Identify the task type:
- Model design (database schema, relationships, migrations)
- View/API development (FBV, CBV, DRF viewsets, serializers)
- Query optimization (N+1 problems, database performance)
- Authentication/permissions (user models, access control)
- Testing (unit tests, integration tests, fixtures)
- Security review (CSRF, XSS, SQL injection, permissions)
- Production deployment (settings, HTTPS, database, caching, monitoring)
- Template rendering (Django templates, context processors)
Leverage available context:
- If
django-ai-boostMCP server is available, use it to understand project structure and existing patterns - Read relevant existing code to understand conventions
- Check Django version for compatibility considerations
2. Load Relevant Reference Documentation
Based on the task type, reference the appropriate bundled documentation:
- Models/ORM work ->
references/models-and-orm.md - Model design patterns and field choices
- Relationship configurations (ForeignKey, ManyToMany)
- Custom managers and QuerySet methods
- Migration strategies
- View/API development ->
references/views-and-urls.md+references/drf-guidelines.md - FBV vs CBV decision criteria
- DRF serializers, viewsets, and routers
- URL configuration patterns
- Middleware and request/response handling
- Performance issues ->
references/performance-optimization.md - Query optimization techniques (select_related, prefetch_related)
- Caching strategies (Redis, Memcached, database caching)
- Database indexing and query profiling
- Connection pooling and async patterns
- Production deployment ->
references/production-deployment.md - Critical settings (DEBUG, SECRET_KEY, ALLOWED_HOSTS)
- HTTPS and SSL/TLS configuration
- Database and cache configuration
- Static/media file serving
- Error monitoring and logging
- Deployment process and health checks
- Security concerns ->
references/security-checklist.md - CSRF/XSS/SQL injection prevention
- Authentication and authorization patterns
- Secure configuration practices
- Input validation and sanitization
- Testing tasks ->
references/testing-strategies.md - Test structure and organization
- Fixtures and factories
- Mocking external dependencies
- Coverage and CI/CD integration
3. Implement Following Django Best Practices
Code quality standards:
- Follow PEP 8 and Django coding style
- Use Django built-ins over third-party packages when possible
- Keep views thin, use services/managers for business logic
- Write descriptive variable names and add docstrings for complex logic
- Handle errors gracefully with appropriate exceptions
Django-specific patterns:
- Use
select_related()for FK/OneToOne,prefetch_related()for reverse FK/M2M - Leverage class-based views and mixins for code reuse
- Use Django forms/serializers for validation
- Follow Django's migration workflow (never edit applied migrations)
- Use Django's built-in security features (CSRF tokens, auth decorators)
API development (DRF):
- Use ModelSerializer for standard CRUD operations
- Implement proper pagination and filtering
- Use appropriate permission classes
- Follow RESTful conventions for endpoints
- Version APIs when making breaking changes
4. Validate and Test
Before presenting the solution:
Code review:
- Check for N+1 query problems (use Django Debug Toolbar mentally)
- Verify proper error handling and edge cases
- Ensure security best practices are followed
- Confirm migrations are clean and reversible
Testing considerations:
- Suggest or write appropriate tests for new functionality
- Verify test coverage for critical paths
- Check that fixtures/factories are maintainable
Performance check:
- Review database queries for efficiency
- Consider caching opportunities
- Verify proper use of database indexes
Bundled Resources
references/ - Comprehensive Django documentation loaded into context as needed
These reference files provide detailed guidance beyond this SKILL.md overview:
- `references/models-and-orm.md` (~11k words)
- Model field types and best practices
- Relationship configurations (ForeignKey, OneToOne, ManyToMany)
- Custom managers and QuerySet methods
- Migration patterns and common pitfalls
- Database-level constraints and indexes
- `references/views-and-urls.md` (~17k words)
- Function-based vs class-based view trade-offs
- CBV mixins and inheritance patterns
- URL routing and reverse resolution
- Middleware implementation
- Request/response lifecycle
- `references/drf-guidelines.md` (~18k words)
- Serializer patterns (ModelSerializer, nested serializers)
- ViewSet and router configurations
- Pagination, filtering, and search
- Authentication and permission classes
- API versioning strategies
- Performance optimization for APIs
- `references/testing-strategies.md` (~18k words)
- Test organization and structure
- Factory patterns vs fixtures
- Testing views, models, and serializers
- Mocking external services
- Test database optimization
- CI/CD integration
- `references/security-checklist.md` (~12k words)
- CSRF protection implementation
- XSS prevention techniques
- SQL injection defense
- Authentication best practices
- Permission and authorization patterns
- Secure settings configuration
- `references/performance-optimization.md` (~14k words)
- Query optimization (select_related, prefetch_related, only, defer)
- Database indexing strategies
- Caching layers (Redis, Memcached, database cache)
- Database connection pooling
- Profiling and monitoring tools
- Async views and background tasks
- `references/production-deployment.md` (~20k words)
- Critical settings (DEBUG, SECRET_KEY, ALLOWED_HOSTS)
- Database configuration and connection pooling
- HTTPS/SSL configuration and security headers
- Static and media file serving
- Caching with Redis/Memcached
- Email configuration for production
- Error monitoring with Sentry
- Logging and health checks
- Zero-downtime deployment strategies
- `references/examples.md` - Practical implementation examples
- Model design with custom managers
- N+1 query optimization
- DRF API endpoint implementation
- Writing Django tests
Additional Notes
Django Version Compatibility:
- Consider LTS releases (4.2, 5.2) for production
- Check deprecation warnings when upgrading
- Use
django-upgradetool for automated migration
Common Pitfalls to Avoid:
- Circular imports (use lazy references)
- Missing
related_nameon relationships - Forgetting database indexes on frequently queried fields
- Using
get()without exception handling - N+1 queries in templates and serializers
Django REST Framework Best Practices
Serializers
ModelSerializer Basics
from rest_framework import serializers
from .models import Post, Comment
# ✅ GOOD: Basic ModelSerializer
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = ['id', 'title', 'content', 'author', 'created_at']
read_only_fields = ['id', 'created_at', 'author']
# ✅ GOOD: Exclude sensitive fields
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email', 'first_name', 'last_name']
# NOT password, last_login, etc.
# ❌ BAD: Using __all__ exposes everything
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = '__all__' # Exposes password hash!Rule: Explicitly list fields. Never use fields = '__all__' in production.
Nested Serializers
# ✅ GOOD: Nested read-only serializer
class CommentSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True)
class Meta:
model = Comment
fields = ['id', 'content', 'author', 'created_at']
class PostSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True)
comments = CommentSerializer(many=True, read_only=True)
class Meta:
model = Post
fields = ['id', 'title', 'content', 'author', 'comments', 'created_at']
# ✅ GOOD: Different serializers for read/write
class PostListSerializer(serializers.ModelSerializer):
"""Lightweight serializer for list view."""
author_name = serializers.CharField(source='author.username', read_only=True)
class Meta:
model = Post
fields = ['id', 'title', 'author_name', 'created_at']
class PostDetailSerializer(serializers.ModelSerializer):
"""Detailed serializer with nested data."""
author = UserSerializer(read_only=True)
comments = CommentSerializer(many=True, read_only=True)
class Meta:
model = Post
fields = ['id', 'title', 'content', 'author', 'comments', 'created_at', 'updated_at']Custom Fields and Validation
# ✅ GOOD: Add computed fields
class PostSerializer(serializers.ModelSerializer):
comment_count = serializers.SerializerMethodField()
is_author = serializers.SerializerMethodField()
class Meta:
model = Post
fields = ['id', 'title', 'content', 'comment_count', 'is_author']
def get_comment_count(self, obj):
return obj.comments.count()
def get_is_author(self, obj):
request = self.context.get('request')
return request.user == obj.author if request else False
# ✅ GOOD: Field-level validation
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = ['title', 'content']
def validate_title(self, value):
if len(value) < 5:
raise serializers.ValidationError("Title must be at least 5 characters")
return value
# ✅ GOOD: Object-level validation
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = ['title', 'content', 'is_published']
def validate(self, data):
if data.get('is_published') and not data.get('content'):
raise serializers.ValidationError(
"Cannot publish post without content"
)
return dataWrite-Only and Read-Only Fields
# ✅ GOOD: Password handling
class UserRegistrationSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True, min_length=8)
password_confirm = serializers.CharField(write_only=True)
class Meta:
model = User
fields = ['username', 'email', 'password', 'password_confirm']
def validate(self, data):
if data['password'] != data['password_confirm']:
raise serializers.ValidationError("Passwords don't match")
return data
def create(self, validated_data):
validated_data.pop('password_confirm')
user = User.objects.create_user(**validated_data)
return user
# ✅ GOOD: Read-only computed field
class PostSerializer(serializers.ModelSerializer):
author_name = serializers.CharField(source='author.username', read_only=True)
url = serializers.HyperlinkedIdentityField(view_name='post-detail', read_only=True)
class Meta:
model = Post
fields = ['id', 'title', 'content', 'author_name', 'url']ViewSets
ModelViewSet
from rest_framework import viewsets, filters
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from rest_framework.decorators import action
from django_filters.rest_framework import DjangoFilterBackend
# ✅ GOOD: Complete CRUD ViewSet
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
permission_classes = [IsAuthenticatedOrReadOnly]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['author', 'is_published']
search_fields = ['title', 'content']
ordering_fields = ['created_at', 'title']
ordering = ['-created_at']
def get_serializer_class(self):
"""Use different serializers for list vs detail."""
if self.action == 'list':
return PostListSerializer
return PostDetailSerializer
def get_queryset(self):
"""Optimize queries based on action."""
queryset = super().get_queryset()
if self.action == 'list':
return queryset.select_related('author').only(
'id', 'title', 'created_at', 'author__username'
)
elif self.action == 'retrieve':
return queryset.select_related('author').prefetch_related('comments')
return queryset
def perform_create(self, serializer):
"""Set author automatically."""
serializer.save(author=self.request.user)
# ✅ GOOD: Custom action
@action(detail=True, methods=['post'])
def publish(self, request, pk=None):
post = self.get_object()
if post.author != request.user:
return Response({'error': 'Not authorized'}, status=403)
post.is_published = True
post.save()
return Response({'status': 'published'})
@action(detail=False, methods=['get'])
def my_posts(self, request):
"""Get posts by current user."""
posts = self.get_queryset().filter(author=request.user)
serializer = self.get_serializer(posts, many=True)
return Response(serializer.data)ReadOnlyModelViewSet
# ✅ GOOD: Read-only API
class CategoryViewSet(viewsets.ReadOnlyModelViewSet):
"""Only allow GET requests (list and retrieve)."""
queryset = Category.objects.all()
serializer_class = CategorySerializer
permission_classes = [AllowAny]APIView for Custom Logic
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
# ✅ GOOD: Custom API endpoint
class PostStatisticsAPIView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request):
user = request.user
stats = {
'total_posts': Post.objects.filter(author=user).count(),
'published_posts': Post.objects.filter(author=user, is_published=True).count(),
'total_comments': Comment.objects.filter(post__author=user).count(),
}
return Response(stats)Permissions
Built-in Permissions
from rest_framework.permissions import (
IsAuthenticated,
IsAuthenticatedOrReadOnly,
AllowAny,
IsAdminUser,
)
# ✅ GOOD: Require authentication for all actions
class PostViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated]
...
# ✅ GOOD: Read-only for anonymous, write for authenticated
class PostViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticatedOrReadOnly]
...Custom Permissions
from rest_framework import permissions
# ✅ GOOD: Object-level permission
class IsAuthorOrReadOnly(permissions.BasePermission):
"""Only author can edit/delete."""
def has_object_permission(self, request, view, obj):
# Read permissions for everyone
if request.method in permissions.SAFE_METHODS:
return True
# Write permissions only for author
return obj.author == request.user
# ✅ GOOD: Staff or owner permission
class IsStaffOrOwner(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return request.user.is_staff or obj.author == request.user
# Usage
class PostViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated, IsAuthorOrReadOnly]
...Per-Action Permissions
# ✅ GOOD: Different permissions per action
class PostViewSet(viewsets.ModelViewSet):
def get_permissions(self):
if self.action in ['list', 'retrieve']:
return [AllowAny()]
elif self.action == 'create':
return [IsAuthenticated()]
else: # update, partial_update, destroy
return [IsAuthenticated(), IsAuthorOrReadOnly()]Authentication
Token Authentication
# settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
],
}
# views.py
from rest_framework.authtoken.models import Token
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
@api_view(['POST'])
@permission_classes([AllowAny])
def login(request):
username = request.data.get('username')
password = request.data.get('password')
user = authenticate(username=username, password=password)
if user:
token, created = Token.objects.get_or_create(user=user)
return Response({'token': token.key})
return Response({'error': 'Invalid credentials'}, status=400)Pagination
# settings.py
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 20,
}
# Custom pagination
from rest_framework.pagination import PageNumberPagination
class StandardResultsSetPagination(PageNumberPagination):
page_size = 20
page_size_query_param = 'page_size'
max_page_size = 100
class PostViewSet(viewsets.ModelViewSet):
pagination_class = StandardResultsSetPagination
...Filtering, Searching, Ordering
pip install django-filter# settings.py
INSTALLED_APPS = [
...
'django_filters',
]
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
'rest_framework.filters.OrderingFilter',
],
}
# views.py
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters
# ✅ GOOD: Complete filtering setup
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
# Exact match filtering
filterset_fields = ['author', 'category', 'is_published']
# Full-text search
search_fields = ['title', 'content', 'author__username']
# Ordering
ordering_fields = ['created_at', 'title', 'view_count']
ordering = ['-created_at'] # Default ordering
# Usage:
# GET /api/posts/?author=1&is_published=true
# GET /api/posts/?search=django
# GET /api/posts/?ordering=-created_at
# GET /api/posts/?author=1&search=tutorial&ordering=titleAdvanced Filtering
from django_filters import rest_framework as filters
# ✅ GOOD: Custom FilterSet
class PostFilter(filters.FilterSet):
title = filters.CharFilter(lookup_expr='icontains')
created_after = filters.DateTimeFilter(field_name='created_at', lookup_expr='gte')
created_before = filters.DateTimeFilter(field_name='created_at', lookup_expr='lte')
min_views = filters.NumberFilter(field_name='view_count', lookup_expr='gte')
class Meta:
model = Post
fields = ['author', 'category', 'is_published']
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
filterset_class = PostFilterThrottling (Rate Limiting)
# settings.py
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle',
],
'DEFAULT_THROTTLE_RATES': {
'anon': '100/day',
'user': '1000/day',
},
}
# Custom throttle
from rest_framework.throttling import UserRateThrottle
class BurstRateThrottle(UserRateThrottle):
scope = 'burst'
class SustainedRateThrottle(UserRateThrottle):
scope = 'sustained'
# settings.py
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_RATES': {
'burst': '10/min',
'sustained': '100/hour',
},
}
# views.py
class PostViewSet(viewsets.ModelViewSet):
throttle_classes = [BurstRateThrottle, SustainedRateThrottle]
...Versioning
# settings.py
REST_FRAMEWORK = {
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
'DEFAULT_VERSION': 'v1',
'ALLOWED_VERSIONS': ['v1', 'v2'],
}
# urls.py
urlpatterns = [
path('api/v1/', include('api.urls', namespace='v1')),
path('api/v2/', include('api.urls', namespace='v2')),
]
# views.py
class PostViewSet(viewsets.ModelViewSet):
def get_serializer_class(self):
if self.request.version == 'v2':
return PostSerializerV2
return PostSerializerError Handling
from rest_framework.views import exception_handler
from rest_framework.response import Response
# ✅ GOOD: Custom exception handler
def custom_exception_handler(exc, context):
response = exception_handler(exc, context)
if response is not None:
# Customize error response format
response.data = {
'error': response.data,
'status_code': response.status_code,
}
return response
# settings.py
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'myapp.exceptions.custom_exception_handler',
}
# Raise custom exceptions
from rest_framework.exceptions import ValidationError, PermissionDenied
def my_view(request):
if not condition:
raise ValidationError('Invalid data provided')
if not has_permission:
raise PermissionDenied('You do not have permission')Testing DRF APIs
from rest_framework.test import APITestCase, APIClient
from rest_framework import status
# ✅ GOOD: API test case
class PostAPITestCase(APITestCase):
def setUp(self):
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.client = APIClient()
def test_list_posts(self):
"""Test retrieving post list."""
Post.objects.create(title='Test Post', author=self.user)
response = self.client.get('/api/posts/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data['results']), 1)
def test_create_post_authenticated(self):
"""Test creating post when authenticated."""
self.client.force_authenticate(user=self.user)
data = {'title': 'New Post', 'content': 'Test content'}
response = self.client.post('/api/posts/', data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Post.objects.count(), 1)
self.assertEqual(Post.objects.first().author, self.user)
def test_create_post_unauthenticated(self):
"""Test creating post when not authenticated."""
data = {'title': 'New Post', 'content': 'Test content'}
response = self.client.post('/api/posts/', data)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test_update_own_post(self):
"""Test updating own post."""
post = Post.objects.create(title='Original', author=self.user)
self.client.force_authenticate(user=self.user)
data = {'title': 'Updated'}
response = self.client.patch(f'/api/posts/{post.id}/', data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
post.refresh_from_db()
self.assertEqual(post.title, 'Updated')
def test_delete_others_post(self):
"""Test cannot delete another user's post."""
other_user = User.objects.create_user(username='other', password='pass')
post = Post.objects.create(title='Post', author=other_user)
self.client.force_authenticate(user=self.user)
response = self.client.delete(f'/api/posts/{post.id}/')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)DRF Best Practices Checklist
✅ Explicitly define serializer fields (never use __all__) ✅ Use read_only_fields for computed or auto-generated fields ✅ Create separate serializers for list/detail views ✅ Use select_related() and prefetch_related() in ViewSet querysets ✅ Implement proper permissions (object-level when needed) ✅ Add authentication to all non-public endpoints ✅ Implement pagination for list endpoints ✅ Add filtering, searching, and ordering where appropriate ✅ Use throttling to prevent API abuse ✅ Version your API from the start ✅ Write comprehensive API tests ✅ Document your API (use drf-spectacular or similar) ✅ Validate all input data in serializers ✅ Return appropriate HTTP status codes
---
Remember: DRF gives you powerful tools, but you must configure them properly. Security and performance are not automatic - you must implement them deliberately.
Django Expert - Examples
Practical examples demonstrating Django best practices for common development scenarios.
Example 1: Model Design with Custom Manager
User Request:
"Create a User model with email authentication, profile tracking, and a way to query active users"
Claude Response:
I'll create a custom User model following Django best practices:
1. Extend AbstractUser for email-based authentication
2. Add custom manager for common queries
3. Include proper Meta configuration with indexes
4. Add timestamp tracking fieldsImplementation:
# models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
class UserManager(models.Manager):
"""Custom manager for User model with common query methods."""
def active_users(self):
"""Return users who are active and email verified."""
return self.filter(is_active=True, email_verified=True)
def get_by_email(self, email):
"""Case-insensitive email lookup."""
return self.get(email__iexact=email)
class User(AbstractUser):
email = models.EmailField(unique=True, db_index=True)
email_verified = models.BooleanField(default=False)
profile_completed = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
updated_at = models.DateTimeField(auto_now=True)
objects = UserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']
class Meta:
db_table = 'users'
ordering = ['-created_at']
indexes = [
models.Index(fields=['email', 'is_active']),
models.Index(fields=['created_at']),
]
def __str__(self):
return self.email
def get_full_name(self):
"""Return user's full name or email if name not set."""
full_name = f"{self.first_name} {self.last_name}".strip()
return full_name if full_name else self.emailNext steps:
- Run
python manage.py makemigrationsto create migrations - Update settings.py:
AUTH_USER_MODEL = 'yourapp.User' - Create email verification workflow if needed
---
Example 2: Query Optimization (N+1 Problem)
User Request:
"This blog post list view is generating 100+ database queries and it's really slow"
Current Code (Problematic):
# BAD: N+1 query problem
def post_list(request):
posts = Post.objects.all() # 1 query
return render(request, 'posts/list.html', {'posts': posts})
# Template causes N+1:
# {% for post in posts %}
# {{ post.author.name }} # 1 query per post
# {{ post.comments.count }} # 1 query per post
# {% endfor %}Claude Response:
I can see the N+1 query problem. The view fetches posts, but the template
accesses related `author` (ForeignKey) and `comments` (reverse ForeignKey)
for each post, causing additional queries.
Solution: Use select_related() for ForeignKey and prefetch_related() for
reverse relationships.Optimized Code:
# GOOD: Optimized with proper eager loading
from django.db.models import Count, Prefetch
def post_list(request):
posts = Post.objects.select_related(
'author' # ForeignKey - use select_related (SQL JOIN)
).prefetch_related(
Prefetch(
'comments',
queryset=Comment.objects.select_related('author').filter(is_approved=True)
)
).annotate(
comment_count=Count('comments', filter=Q(comments__is_approved=True))
).order_by('-published_at')
return render(request, 'posts/list.html', {'posts': posts})
# Now template only uses the prefetched data - no additional queries!Performance Impact:
- Before: 1 + N (authors) + N (comment counts) = 201 queries for 100 posts
- After: 3 queries total (posts, authors via JOIN, comments prefetch)
---
Example 3: Django REST Framework API Endpoint
User Request:
"Create a REST API for my Product model with filtering, pagination, and proper permissions"
Claude Response:
I'll implement a complete DRF endpoint following best practices:
1. Create ModelSerializer with validation
2. Implement ViewSet with proper permissions
3. Add filtering and search capabilities
4. Configure pagination
5. Set up URL routingImplementation:
# serializers.py
from rest_framework import serializers
from .models import Product
class ProductSerializer(serializers.ModelSerializer):
category_name = serializers.CharField(source='category.name', read_only=True)
is_available = serializers.SerializerMethodField()
class Meta:
model = Product
fields = [
'id', 'name', 'description', 'price', 'stock',
'category', 'category_name', 'is_available', 'created_at'
]
read_only_fields = ['id', 'created_at']
def get_is_available(self, obj):
"""Check if product is in stock."""
return obj.stock > 0
def validate_price(self, value):
"""Ensure price is positive."""
if value <= 0:
raise serializers.ValidationError("Price must be greater than zero")
return value
# views.py
from rest_framework import viewsets, filters
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from django_filters.rest_framework import DjangoFilterBackend
from .models import Product
from .serializers import ProductSerializer
class ProductViewSet(viewsets.ModelViewSet):
"""
ViewSet for Product CRUD operations.
List and retrieve are public, create/update/delete require authentication.
"""
queryset = Product.objects.select_related('category').filter(is_active=True)
serializer_class = ProductSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['category', 'stock']
search_fields = ['name', 'description']
ordering_fields = ['price', 'created_at', 'stock']
ordering = ['-created_at']
def get_queryset(self):
"""Filter products based on query params."""
queryset = super().get_queryset()
# Filter by price range
min_price = self.request.query_params.get('min_price')
max_price = self.request.query_params.get('max_price')
if min_price:
queryset = queryset.filter(price__gte=min_price)
if max_price:
queryset = queryset.filter(price__lte=max_price)
return queryset
# urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import ProductViewSet
router = DefaultRouter()
router.register(r'products', ProductViewSet, basename='product')
urlpatterns = [
path('api/', include(router.urls)),
]
# settings.py (pagination configuration)
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 20,
}API Usage Examples:
# List all products (paginated)
GET /api/products/
# Search products
GET /api/products/?search=laptop
# Filter by category and price range
GET /api/products/?category=electronics&min_price=100&max_price=500
# Order by price ascending
GET /api/products/?ordering=price
# Retrieve single product
GET /api/products/123/
# Create product (requires authentication)
POST /api/products/---
Example 4: Writing Django Tests
User Request:
"Write tests for the Product API endpoint"
Implementation:
# tests/test_product_api.py
from django.test import TestCase
from django.contrib.auth import get_user_model
from rest_framework.test import APIClient
from rest_framework import status
from decimal import Decimal
from .models import Product, Category
User = get_user_model()
class ProductAPITestCase(TestCase):
"""Test suite for Product API endpoints."""
def setUp(self):
"""Set up test data before each test."""
self.client = APIClient()
self.user = User.objects.create_user(
email='test@example.com',
password='testpass123'
)
self.category = Category.objects.create(name='Electronics')
self.product = Product.objects.create(
name='Test Product',
description='A test product',
price=Decimal('99.99'),
stock=10,
category=self.category
)
def test_list_products_unauthenticated(self):
"""Unauthenticated users can list products."""
response = self.client.get('/api/products/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data['results']), 1)
def test_create_product_requires_authentication(self):
"""Creating products requires authentication."""
data = {
'name': 'New Product',
'price': '49.99',
'stock': 5,
'category': self.category.id
}
response = self.client.post('/api/products/', data)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test_create_product_authenticated(self):
"""Authenticated users can create products."""
self.client.force_authenticate(user=self.user)
data = {
'name': 'New Product',
'description': 'A new product',
'price': '49.99',
'stock': 5,
'category': self.category.id
}
response = self.client.post('/api/products/', data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Product.objects.count(), 2)
self.assertEqual(response.data['name'], 'New Product')
def test_filter_by_price_range(self):
"""Products can be filtered by price range."""
Product.objects.create(
name='Expensive Product',
price=Decimal('299.99'),
stock=5,
category=self.category
)
response = self.client.get('/api/products/?min_price=200')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data['results']), 1)
self.assertEqual(response.data['results'][0]['name'], 'Expensive Product')
def test_product_availability_field(self):
"""is_available field correctly reflects stock status."""
response = self.client.get(f'/api/products/{self.product.id}/')
self.assertEqual(response.data['is_available'], True)
# Update stock to 0
self.product.stock = 0
self.product.save()
response = self.client.get(f'/api/products/{self.product.id}/')
self.assertEqual(response.data['is_available'], False)Run tests:
python manage.py test tests.test_product_apiDjango ORM Best Practices
The N+1 Query Problem
The most common performance issue in Django applications.
Problem Example
# ❌ BAD: N+1 queries (1 + N where N = number of users)
users = User.objects.all() # 1 query
for user in users:
print(user.profile.bio) # N additional queries!This results in:
SELECT * FROM users; -- 1 query
SELECT * FROM profiles WHERE user_id=1; -- Query for each user
SELECT * FROM profiles WHERE user_id=2;
SELECT * FROM profiles WHERE user_id=3;
-- ... etcSolution: select_related()
Use select_related() for foreign key and one-to-one relationships:
# ✅ GOOD: 1 query with JOIN
users = User.objects.select_related('profile').all()
for user in users:
print(user.profile.bio) # No additional queries!This results in:
SELECT *
FROM users
INNER JOIN profiles ON users.id = profiles.user_id; -- 1 querySolution: prefetch_related()
Use prefetch_related() for many-to-many and reverse foreign key relationships:
# ❌ BAD: N+1 queries
users = User.objects.all()
for user in users:
for group in user.groups.all(): # N queries!
print(group.name)
# ✅ GOOD: 2 queries total
users = User.objects.prefetch_related('groups').all()
for user in users:
for group in user.groups.all(): # No additional queries!
print(group.name)This results in:
SELECT * FROM users; -- Query 1
SELECT * FROM groups
INNER JOIN user_groups ON groups.id = user_groups.group_id
WHERE user_groups.user_id IN (1, 2, 3, ...); -- Query 2Combining select_related() and prefetch_related()
# Get users with their profiles AND groups in 2 queries
users = User.objects.select_related('profile').prefetch_related('groups').all()Advanced Prefetching
Prefetch with Filtering
from django.db.models import Prefetch
# Only prefetch active posts
users = User.objects.prefetch_related(
Prefetch(
'posts',
queryset=Post.objects.filter(is_published=True).order_by('-created_at')
)
).all()Nested Prefetching
# Get users with their posts and each post's comments
users = User.objects.prefetch_related(
'posts',
'posts__comments',
'posts__comments__author'
).all()Custom Managers
Encapsulate common queries in custom managers:
class UserManager(models.Manager):
def active(self):
"""Get active users."""
return self.filter(is_active=True)
def with_profile(self):
"""Always include profile to prevent N+1."""
return self.select_related('profile')
def with_full_details(self):
"""Get users with all related data."""
return self.select_related('profile').prefetch_related(
'groups',
'user_permissions',
Prefetch(
'posts',
queryset=Post.objects.filter(is_published=True)
)
)
class User(AbstractUser):
objects = UserManager()
# Usage
active_users = User.objects.active().with_profile()
detailed_users = User.objects.with_full_details()QuerySet Methods
only() and defer()
Load only specific fields when you don't need the entire model:
# only() - Load ONLY specified fields
users = User.objects.only('id', 'email', 'first_name')
# SELECT id, email, first_name FROM users
# defer() - Load ALL fields EXCEPT specified
users = User.objects.defer('password', 'last_login')
# SELECT id, email, first_name, ... FROM users (excludes password, last_login)Warning: Accessing deferred fields triggers an additional query!
users = User.objects.defer('bio').all()
for user in users:
print(user.bio) # Additional query per user!values() and values_list()
Get dictionaries or tuples instead of model instances (faster):
# values() - Returns dictionaries
users = User.objects.values('id', 'email')
# [{'id': 1, 'email': 'user@example.com'}, ...]
# values_list() - Returns tuples
user_ids = User.objects.values_list('id', flat=True)
# [1, 2, 3, 4, ...]
user_data = User.objects.values_list('id', 'email')
# [(1, 'user@example.com'), (2, 'other@example.com'), ...]exists() and count()
# ❌ BAD: Loads all objects into memory
if len(User.objects.filter(email=email)):
...
# ✅ GOOD: Database-level check
if User.objects.filter(email=email).exists():
...
# ❌ BAD: Loads all objects to count
total = len(User.objects.all())
# ✅ GOOD: Database-level count
total = User.objects.count()Aggregation & Annotation
aggregate()
Get summary statistics:
from django.db.models import Count, Avg, Max, Min, Sum
# Get statistics about all users
stats = User.objects.aggregate(
total=Count('id'),
avg_posts=Avg('posts__count'),
max_created=Max('created_at')
)
# {'total': 100, 'avg_posts': 5.2, 'max_created': datetime(...)}annotate()
Add calculated fields to each object:
from django.db.models import Count, Q
# Add post count to each user
users = User.objects.annotate(
total_posts=Count('posts'),
published_posts=Count('posts', filter=Q(posts__is_published=True))
)
for user in users:
print(f"{user.email}: {user.total_posts} total, {user.published_posts} published")Bulk Operations
bulk_create()
Create multiple objects in one query:
# ❌ BAD: N queries
for i in range(1000):
User.objects.create(email=f"user{i}@example.com")
# ✅ GOOD: 1 query
users = [
User(email=f"user{i}@example.com")
for i in range(1000)
]
User.objects.bulk_create(users, batch_size=500)Note: bulk_create() doesn't call save() or send signals!
bulk_update()
Update multiple objects in one query:
# ❌ BAD: N queries
for user in users:
user.is_active = False
user.save()
# ✅ GOOD: 1 query
for user in users:
user.is_active = False
User.objects.bulk_update(users, ['is_active'], batch_size=500)update()
Update multiple objects with one query:
# ❌ BAD: N queries
for user in User.objects.filter(is_active=True):
user.last_login = timezone.now()
user.save()
# ✅ GOOD: 1 query
User.objects.filter(is_active=True).update(last_login=timezone.now())Note: update() doesn't call save() or send signals!
Database Indexes
Add indexes for fields used in:
- Filtering (
WHEREclauses) - Ordering (
ORDER BY) - Joins (
FOREIGN KEY)
class User(models.Model):
email = models.EmailField(unique=True) # Automatic index
created_at = models.DateTimeField(auto_now_add=True)
is_active = models.BooleanField(default=True)
class Meta:
indexes = [
models.Index(fields=['created_at']), # Single field
models.Index(fields=['is_active', 'created_at']), # Composite
models.Index(fields=['-created_at']), # Descending order
]When to Add Indexes
✅ Add indexes for:
- Foreign keys (automatically indexed)
- Fields used in
filter()frequently - Fields used in
order_by()frequently - Unique fields (automatically indexed)
❌ Don't add indexes for:
- Fields that are rarely queried
- Small tables (< 1000 rows)
- Fields that change frequently
- Too many indexes (slows down writes)
Query Optimization Patterns
1. Filter Early, Annotate/Aggregate Late
# ✅ GOOD: Filter before annotation
active_users = User.objects.filter(is_active=True).annotate(
post_count=Count('posts')
)
# ❌ BAD: Annotate then filter (processes all rows first)
active_users = User.objects.annotate(
post_count=Count('posts')
).filter(is_active=True)2. Use iterator() for Large QuerySets
# ❌ BAD: Loads all 1M users into memory
for user in User.objects.all():
process_user(user)
# ✅ GOOD: Streams users in chunks
for user in User.objects.all().iterator(chunk_size=1000):
process_user(user)3. Avoid Chaining Multiple Queries
# ❌ BAD: Multiple queries
active_users = User.objects.filter(is_active=True)
verified_users = active_users.filter(email_verified=True)
recent_users = verified_users.filter(created_at__gte=last_week)
# ✅ GOOD: Single query
recent_users = User.objects.filter(
is_active=True,
email_verified=True,
created_at__gte=last_week
)Transactions
Use transactions for operations that must succeed or fail together:
from django.db import transaction
# Atomic decorator
@transaction.atomic
def create_user_with_profile(data):
user = User.objects.create(**data['user'])
Profile.objects.create(user=user, **data['profile'])
return user
# Atomic context manager
def transfer_credits(from_user, to_user, amount):
with transaction.atomic():
from_user.credits -= amount
from_user.save()
to_user.credits += amount
to_user.save()
# Rollback on error
try:
with transaction.atomic():
User.objects.create(email=email)
send_welcome_email(email) # If this fails, user creation is rolled back
except Exception:
# Transaction automatically rolled back
logger.exception("Failed to create user")Raw SQL (When Necessary)
Sometimes the ORM can't express complex queries efficiently:
# Use raw SQL as a last resort
users = User.objects.raw('''
SELECT u.*, COUNT(p.id) as post_count
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
GROUP BY u.id
HAVING COUNT(p.id) > 10
''')
# ALWAYS use parameters to prevent SQL injection
users = User.objects.raw(
'SELECT * FROM users WHERE created_at > %s',
[start_date]
)Debugging Queries
See Generated SQL
queryset = User.objects.filter(is_active=True)
print(queryset.query) # Print SQL
# Or in shell
from django.db import connection
print(connection.queries) # All queries executedDjango Debug Toolbar
Install and use django-debug-toolbar in development:
pip install django-debug-toolbarShows:
- Number of queries per request
- Duplicate queries
- Slow queries
- SQL for each query
Performance Checklist
Before deploying a feature:
✅ Check for N+1 queries ✅ Use select_related() for FK/OneToOne ✅ Use prefetch_related() for M2M/reverse FK ✅ Add database indexes for filtered/ordered fields ✅ Use bulk operations for creating/updating many objects ✅ Use only()/defer() when loading partial models ✅ Use exists() instead of if queryset ✅ Use count() instead of len(queryset) ✅ Wrap multi-step operations in transactions ✅ Test with production-sized data
---
Remember: Premature optimization is the root of all evil, but N+1 queries are not premature - they're fundamental!
Django Performance Optimization
Database Query Optimization
Avoid N+1 Queries (Most Important!)
# ❌ BAD: N+1 queries
posts = Post.objects.all()
for post in posts:
print(post.author.username) # N additional queries!
# ✅ GOOD: 1 query with JOIN
posts = Post.objects.select_related('author').all()
for post in posts:
print(post.author.username) # No additional queries!See models-and-orm.md for comprehensive coverage of select_related, prefetch_related, and query optimization.
Use only() and defer()
# ✅ GOOD: Load only needed fields
users = User.objects.only('id', 'username', 'email')
# ✅ GOOD: Exclude large fields
posts = Post.objects.defer('content') # Skip content fieldDatabase Indexes
class Post(models.Model):
title = models.CharField(max_length=200)
created_at = models.DateTimeField(auto_now_add=True)
is_published = models.BooleanField(default=False)
view_count = models.IntegerField(default=0)
class Meta:
indexes = [
models.Index(fields=['created_at']), # For ordering
models.Index(fields=['is_published', 'created_at']), # Composite
models.Index(fields=['-view_count']), # For popular posts
]
# ✅ GOOD: Check if index is used
# Run: EXPLAIN ANALYZE SELECT ...
from django.db import connection
queryset = Post.objects.filter(is_published=True).order_by('-created_at')
print(queryset.query)Bulk Operations
# ❌ BAD: N queries
for i in range(1000):
Post.objects.create(title=f'Post {i}')
# ✅ GOOD: 1 query
posts = [Post(title=f'Post {i}') for i in range(1000)]
Post.objects.bulk_create(posts, batch_size=500)
# ❌ BAD: N updates
for post in posts:
post.view_count += 1
post.save()
# ✅ GOOD: 1 query
Post.objects.filter(id__in=post_ids).update(view_count=F('view_count') + 1)Query Profiling
from django.db import connection
from django.test.utils import override_settings
# Check number of queries
with override_settings(DEBUG=True):
posts = Post.objects.select_related('author').all()
list(posts) # Force evaluation
print(f"Queries: {len(connection.queries)}")
for query in connection.queries:
print(query['sql'])Caching
Cache Framework Setup
# settings.py
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
},
'KEY_PREFIX': 'myapp',
'TIMEOUT': 300, # 5 minutes default
}
}Low-Level Cache API
from django.core.cache import cache
# ✅ GOOD: Cache expensive queries
def get_popular_posts():
posts = cache.get('popular_posts')
if posts is None:
posts = Post.objects.filter(
is_published=True
).select_related('author').order_by('-view_count')[:10]
cache.set('popular_posts', posts, timeout=300) # 5 minutes
return posts
# ✅ GOOD: Cache with complex key
def get_user_posts(user_id):
cache_key = f'user_{user_id}_posts'
posts = cache.get(cache_key)
if posts is None:
posts = Post.objects.filter(author_id=user_id)
cache.set(cache_key, posts, timeout=600)
return posts
# Invalidate cache
def create_post(user, data):
post = Post.objects.create(author=user, **data)
cache.delete(f'user_{user.id}_posts') # Invalidate
cache.delete('popular_posts')
return postPer-View Cache
from django.views.decorators.cache import cache_page
# ✅ GOOD: Cache entire view response
@cache_page(60 * 15) # 15 minutes
def blog_list(request):
posts = Post.objects.filter(is_published=True)
return render(request, 'blog/list.html', {'posts': posts})
# ✅ GOOD: Cache with conditional logic
from django.views.decorators.cache import cache_control
@cache_control(max_age=3600, public=True)
def public_content(request):
# Browser and CDN can cache for 1 hour
...Template Fragment Caching
{% load cache %}
{# ✅ GOOD: Cache expensive template sections #}
{% cache 300 sidebar %}
{% for category in categories %}
<li>{{ category.name }} ({{ category.post_count }})</li>
{% endfor %}
{% endcache %}
{# ✅ GOOD: Cache with variables #}
{% cache 600 post_detail post.id post.updated_at %}
<h1>{{ post.title }}</h1>
<div>{{ post.content }}</div>
{% endcache %}Caching Patterns
# ✅ GOOD: Cache with get_or_set
from django.core.cache import cache
def get_post_stats(post_id):
cache_key = f'post_{post_id}_stats'
return cache.get_or_set(
cache_key,
lambda: compute_expensive_stats(post_id),
timeout=3600
)
# ✅ GOOD: Cache invalidation pattern
class Post(models.Model):
...
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
# Invalidate related caches
cache.delete(f'post_{self.id}_detail')
cache.delete('popular_posts')
cache.delete(f'user_{self.author_id}_posts')
def delete(self, *args, **kwargs):
cache.delete(f'post_{self.id}_detail')
cache.delete('popular_posts')
super().delete(*args, **kwargs)Database Connection Pooling
Using pgbouncer (PostgreSQL)
# settings.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'mydb',
'USER': 'myuser',
'PASSWORD': 'mypass',
'HOST': '127.0.0.1',
'PORT': '6432', # pgbouncer port
'CONN_MAX_AGE': 600, # Connection pooling
'OPTIONS': {
'connect_timeout': 10,
},
}
}Django-DB-Pool
pip install django-db-pool# settings.py
DATABASES = {
'default': {
'ENGINE': 'django_db_pool.backends.postgresql',
'POOL_OPTIONS': {
'POOL_SIZE': 10,
'MAX_OVERFLOW': 10,
},
...
}
}Static File Optimization
Compression and Minification
pip install django-compressor# settings.py
INSTALLED_APPS += ['compressor']
COMPRESS_ENABLED = True
COMPRESS_CSS_FILTERS = [
'compressor.filters.css_default.CssAbsoluteFilter',
'compressor.filters.cssmin.CSSMinFilter',
]
COMPRESS_JS_FILTERS = [
'compressor.filters.jsmin.JSMinFilter',
]
STATICFILES_FINDERS = [
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'compressor.finders.CompressorFinder',
]{% load compress %}
{% compress css %}
<link rel="stylesheet" href="{% static 'css/style1.css' %}">
<link rel="stylesheet" href="{% static 'css/style2.css' %}">
{% endcompress %}
{% compress js %}
<script src="{% static 'js/script1.js' %}"></script>
<script src="{% static 'js/script2.js' %}"></script>
{% endcompress %}CDN for Static Files
# settings.py
# Use WhiteNoise for serving static files
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware', # Add this
...
]
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
# Or use S3/CloudFront
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
STATICFILES_STORAGE = 'storages.backends.s3boto3.S3StaticStorage'
AWS_S3_CUSTOM_DOMAIN = 'd123456.cloudfront.net'
STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/static/'Background Tasks with Celery
pip install celery redis# celery.py
from celery import Celery
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
app = Celery('myproject')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks()
# settings.py
CELERY_BROKER_URL = 'redis://localhost:6379/0'
CELERY_RESULT_BACKEND = 'redis://localhost:6379/0'
CELERY_TASK_SERIALIZER = 'json'
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TIMEZONE = 'UTC'
# tasks.py
from celery import shared_task
import time
@shared_task
def send_email(user_id, template):
"""Send email asynchronously."""
user = User.objects.get(id=user_id)
# Send email logic
time.sleep(2) # Simulate email sending
return f'Email sent to {user.email}'
@shared_task
def generate_report(report_id):
"""Generate report in background."""
report = Report.objects.get(id=report_id)
# Generate report logic
report.status = 'completed'
report.save()
return report_id
# views.py
def register(request):
user = User.objects.create_user(...)
# ✅ GOOD: Send email asynchronously
send_email.delay(user.id, 'welcome')
return redirect('home')Pagination
from django.core.paginator import Paginator
# ✅ GOOD: Paginate large querysets
def blog_list(request):
posts = Post.objects.filter(is_published=True).select_related('author')
paginator = Paginator(posts, 20) # 20 posts per page
page_number = request.GET.get('page', 1)
page_obj = paginator.get_page(page_number)
return render(request, 'blog/list.html', {'page_obj': page_obj})
# DRF pagination
# settings.py
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'PAGE_SIZE': 20,
}Middleware Optimization
# ✅ GOOD: Conditional middleware
class ExpensiveMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# Skip for static files
if request.path.startswith('/static/'):
return self.get_response(request)
# Only run for specific paths
if request.path.startswith('/api/'):
# Expensive processing
...
response = self.get_response(request)
return responseTemplate Optimization
Use Template Fragment Caching
{% load cache %}
{# Cache expensive queries #}
{% cache 300 sidebar request.user.id %}
{% include "sidebar.html" %}
{% endcache %}Avoid Logic in Templates
{# ❌ BAD: N+1 queries in template #}
{% for post in posts %}
<h2>{{ post.title }}</h2>
<p>By {{ post.author.username }}</p> {# N queries! #}
<p>{{ post.comments.count }} comments</p> {# N queries! #}
{% endfor %}
{# ✅ GOOD: Preload in view #}
{# posts = Post.objects.select_related('author').annotate(comment_count=Count('comments')) #}
{% for post in posts %}
<h2>{{ post.title }}</h2>
<p>By {{ post.author.username }}</p>
<p>{{ post.comment_count }} comments</p>
{% endfor %}Profiling and Monitoring
Django Debug Toolbar
pip install django-debug-toolbar# settings.py
INSTALLED_APPS += ['debug_toolbar']
MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware']
INTERNAL_IPS = ['127.0.0.1']
# urls.py
from django.conf import settings
if settings.DEBUG:
import debug_toolbar
urlpatterns = [
path('__debug__/', include(debug_toolbar.urls)),
] + urlpatternsShows:
- Number of SQL queries
- Duplicate queries
- Query execution time
- Template rendering time
- Cache hits/misses
Django Silk (Production Profiling)
pip install django-silk# settings.py
INSTALLED_APPS += ['silk']
MIDDLEWARE += ['silk.middleware.SilkyMiddleware']
# urls.py
urlpatterns += [path('silk/', include('silk.urls', namespace='silk'))]Custom Performance Logging
import logging
import time
from functools import wraps
logger = logging.getLogger(__name__)
def log_performance(func):
"""Decorator to log function performance."""
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
duration = time.time() - start
if duration > 1.0: # Log if > 1 second
logger.warning(
f"{func.__name__} took {duration:.2f}s",
extra={'duration': duration}
)
return result
return wrapper
@log_performance
def expensive_view(request):
# View logic
...Database Optimization Checklist
✅ Use select_related() for ForeignKey/OneToOne ✅ Use prefetch_related() for ManyToMany/reverse FK ✅ Add database indexes for filtered/ordered fields ✅ Use only()/defer() for partial model loading ✅ Use values()/values_list() for raw data ✅ Use bulk_create()/bulk_update() for batch operations ✅ Use update() instead of save() when possible ✅ Use exists() instead of counting ✅ Use iterator() for large querysets ✅ Avoid N+1 queries (check with Debug Toolbar)
Caching Checklist
✅ Cache expensive database queries ✅ Cache API responses ✅ Cache template fragments ✅ Use cache versioning for invalidation ✅ Set appropriate cache timeouts ✅ Monitor cache hit rates ✅ Use Redis for production caching ✅ Invalidate cache on data changes
General Performance Checklist
✅ Enable database connection pooling ✅ Compress and minify static files ✅ Use CDN for static files ✅ Enable GZIP compression ✅ Offload heavy tasks to Celery ✅ Paginate large result sets ✅ Profile with Django Debug Toolbar ✅ Monitor production performance (Sentry, New Relic, etc.) ✅ Optimize images (compress, lazy load) ✅ Use HTTP/2 and browser caching headers
---
Remember: Measure first, optimize second. Don't optimize prematurely - profile to find real bottlenecks, then fix them systematically.
Django Production Deployment Reference
This guide covers essential configuration and best practices for deploying Django applications to production environments.
Pre-Deployment Checklist
Run Deployment Checks
Django provides an automated deployment check command:
python manage.py check --deployThis validates critical settings and warns about common misconfigurations.
Rule: Always run check --deploy before going live.
Switch from Development Server
# ❌ NEVER use in production
python manage.py runserver
# ✅ Use production-ready servers
gunicorn myproject.wsgi:application
# or
uvicorn myproject.asgi:application
# or
daphne myproject.asgi:applicationCommon WSGI/ASGI servers:
- Gunicorn: Standard WSGI server, excellent for most Django apps
- uWSGI: High-performance WSGI server with many features
- Uvicorn: ASGI server for async Django (channels, async views)
- Daphne: ASGI server built for Django Channels
---
Critical Settings
SECRET_KEY
Never commit `SECRET_KEY` to version control.
# ✅ GOOD: Load from environment
import os
SECRET_KEY = os.environ["SECRET_KEY"]
# ✅ GOOD: Load from file
with open("/etc/secrets/django_secret_key.txt") as f:
SECRET_KEY = f.read().strip()
# ✅ GOOD: Using python-decouple
from decouple import config
SECRET_KEY = config('SECRET_KEY')
# ❌ BAD: Hardcoded in settings
SECRET_KEY = 'django-insecure-hardcoded-key-123'Key rotation with fallbacks:
SECRET_KEY = os.environ["CURRENT_SECRET_KEY"]
SECRET_KEY_FALLBACKS = [
os.environ["OLD_SECRET_KEY"],
]This allows existing sessions to work during key rotation. Remove old keys from fallbacks after sufficient time.
Rule: Treat SECRET_KEY like a database password.
DEBUG
# ✅ PRODUCTION: Debug disabled
DEBUG = False
# ✅ GOOD: Environment-based
DEBUG = os.environ.get('DJANGO_DEBUG', 'False') == 'True'
# ❌ NEVER in production
DEBUG = TrueWhy `DEBUG = True` is dangerous:
- Exposes source code in error pages
- Shows database queries and local variables
- Reveals settings and library versions
- Significantly impacts performance
Rule: DEBUG = False in production, always.
ALLOWED_HOSTS
# ✅ PRODUCTION: Specific hosts only
ALLOWED_HOSTS = ['example.com', 'www.example.com']
# ✅ GOOD: From environment
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(',')
# ❌ BAD: Allows any host (CSRF vulnerability)
ALLOWED_HOSTS = ['*']
# ❌ BAD: Empty (Django will refuse to run)
ALLOWED_HOSTS = []Nginx configuration to handle invalid hosts:
server {
listen 80 default_server;
server_name _;
return 444; # Close connection without response
}
server {
listen 80;
server_name example.com www.example.com;
# Your Django app configuration
}Rule: Always specify exact hostnames in production.
---
Database Configuration
Connection Settings
# ✅ PRODUCTION: Secure connection parameters
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ['DB_NAME'],
'USER': os.environ['DB_USER'],
'PASSWORD': os.environ['DB_PASSWORD'],
'HOST': os.environ['DB_HOST'],
'PORT': os.environ.get('DB_PORT', '5432'),
'CONN_MAX_AGE': 600, # Persistent connections
'OPTIONS': {
'sslmode': 'require', # Enforce SSL
},
}
}
# ❌ BAD: Hardcoded credentials
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'mydb',
'USER': 'postgres',
'PASSWORD': 'password123',
'HOST': 'localhost',
}
}Connection Pooling
# ✅ GOOD: Persistent connections
DATABASES = {
'default': {
'CONN_MAX_AGE': 600, # Keep connections open for 10 minutes
}
}
# ✅ BETTER: External connection pooler (PgBouncer)
DATABASES = {
'default': {
'HOST': 'pgbouncer-host',
'PORT': 6432,
'CONN_MAX_AGE': None, # Let PgBouncer handle pooling
}
}Rule: Enable persistent connections or use external pooling.
Database Backups
# PostgreSQL backup
pg_dump -h localhost -U user -d database > backup.sql
# Automated backups (example cron)
0 2 * * * pg_dump -h localhost -U user -d database | gzip > /backups/db_$(date +\%Y\%m\%d).sql.gzRule: Set up automated backups before going live.
---
HTTPS Configuration
All production sites must use HTTPS, especially those handling authentication.
SSL/TLS Settings
# ✅ PRODUCTION: Force HTTPS
SECURE_SSL_REDIRECT = True
# ✅ GOOD: Strict Transport Security
SECURE_HSTS_SECONDS = 31536000 # 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# ✅ REQUIRED: Secure cookies
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# ✅ GOOD: Additional cookie security
SESSION_COOKIE_HTTPONLY = True
CSRF_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'
CSRF_COOKIE_SAMESITE = 'Lax'HSTS (HTTP Strict Transport Security) considerations:
- Start with shorter duration for testing:
SECURE_HSTS_SECONDS = 3600 - Increase gradually: 1 week → 1 month → 1 year
- Only enable
PRELOADafter testing with shorter durations - Be aware: HSTS cannot be easily undone
Nginx SSL Configuration
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Rule: Never serve authenticated pages over HTTP.
---
Static and Media Files
Static Files
# settings.py
STATIC_URL = '/static/'
STATIC_ROOT = '/var/www/myproject/static/'
# ✅ GOOD: Use CDN for static files
STATIC_URL = 'https://cdn.example.com/static/'
# ✅ GOOD: Enable compression
STORAGES = {
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}Collect static files before deployment:
python manage.py collectstatic --no-inputNginx static file serving:
location /static/ {
alias /var/www/myproject/static/;
expires 1y;
add_header Cache-Control "public, immutable";
}Rule: Serve static files through CDN or reverse proxy, not Django.
Media Files
# settings.py
MEDIA_URL = '/media/'
MEDIA_ROOT = '/var/www/myproject/media/'
# ✅ BETTER: Use object storage
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
AWS_STORAGE_BUCKET_NAME = 'my-bucket'
AWS_S3_CUSTOM_DOMAIN = 'cdn.example.com'Nginx media file serving:
location /media/ {
alias /var/www/myproject/media/;
# ✅ CRITICAL: Prevent script execution
location ~ \.(php|py|pl|sh|cgi)$ {
deny all;
}
}Security considerations:
- Never execute uploaded files
- Validate file types before saving
- Use unique filenames to prevent overwrites
- Set up backups for user uploads
- Consider virus scanning for uploaded files
Rule: Treat all media files as untrusted user input.
---
Caching
Cache Backend
# ✅ PRODUCTION: Redis cache
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': os.environ.get('REDIS_URL', 'redis://127.0.0.1:6379/1'),
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
'PASSWORD': os.environ.get('REDIS_PASSWORD'),
},
'KEY_PREFIX': 'myapp',
'TIMEOUT': 300,
}
}
# ❌ DEVELOPMENT ONLY: Dummy cache
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
}
}Session Backend
# ✅ PRODUCTION: Cached sessions
SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'
# ✅ ALSO GOOD: Redis sessions
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
SESSION_CACHE_ALIAS = 'default'
# ✅ GOOD: Set reasonable session timeout
SESSION_COOKIE_AGE = 86400 # 24 hoursClear expired sessions regularly:
# Add to cron
python manage.py clearsessionsRule: Use Redis or Memcached for production caching.
Template Caching
# ✅ PRODUCTION: Cached template loader
TEMPLATES = [{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'OPTIONS': {
'loaders': [
('django.template.loaders.cached.Loader', [
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
]),
],
},
}]This is automatically enabled when DEBUG = False, but explicit configuration provides better control.
---
Email Configuration
SMTP Settings
# ✅ PRODUCTION: Proper email configuration
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = os.environ.get('EMAIL_HOST', 'smtp.sendgrid.net')
EMAIL_PORT = int(os.environ.get('EMAIL_PORT', 587))
EMAIL_USE_TLS = True
EMAIL_HOST_USER = os.environ['EMAIL_HOST_USER']
EMAIL_HOST_PASSWORD = os.environ['EMAIL_HOST_PASSWORD']
DEFAULT_FROM_EMAIL = 'noreply@example.com'
SERVER_EMAIL = 'server@example.com'
# ❌ DEVELOPMENT ONLY: Console backend
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'Email services:
- SendGrid: Reliable transactional email
- Mailgun: Good deliverability and analytics
- Amazon SES: Cost-effective for high volume
- Postmark: Excellent for transactional email
Rule: Use a professional email service, not localhost.
---
Error Monitoring and Logging
Logging Configuration
# ✅ PRODUCTION: Comprehensive logging
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
'style': '{',
},
},
'handlers': {
'file': {
'level': 'ERROR',
'class': 'logging.handlers.RotatingFileHandler',
'filename': '/var/log/django/error.log',
'maxBytes': 1024 * 1024 * 10, # 10 MB
'backupCount': 5,
'formatter': 'verbose',
},
'console': {
'level': 'INFO',
'class': 'logging.StreamHandler',
'formatter': 'verbose',
},
},
'loggers': {
'django': {
'handlers': ['file', 'console'],
'level': 'INFO',
'propagate': False,
},
'myapp': {
'handlers': ['file', 'console'],
'level': 'INFO',
'propagate': False,
},
},
}Error Notifications
# ✅ GOOD: Admin notifications
ADMINS = [
('Admin Name', 'admin@example.com'),
]
MANAGERS = [
('Manager Name', 'manager@example.com'),
]
# Filter spurious 404s
IGNORABLE_404_URLS = [
re.compile(r'\.(php|cgi)$'),
re.compile(r'^/phpmyadmin/'),
]Limitations of email notifications:
- Don't scale well with traffic
- Can overwhelm email
- No aggregation or analytics
- Delayed notification
Sentry Integration
# ✅ BETTER: Sentry for error tracking
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
sentry_sdk.init(
dsn=os.environ.get('SENTRY_DSN'),
integrations=[DjangoIntegration()],
traces_sample_rate=0.1, # Performance monitoring
send_default_pii=False, # Don't send personal data
environment=os.environ.get('ENVIRONMENT', 'production'),
)Benefits of Sentry:
- Real-time error tracking
- Error aggregation and deduplication
- Stack trace analysis
- Performance monitoring
- Release tracking
Rule: Use Sentry or similar service for production error monitoring.
---
Security Headers
# ✅ PRODUCTION: Security headers
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
# ✅ GOOD: Content Security Policy
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'", "'unsafe-inline'") # Avoid unsafe-inline in production
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'")
CSP_IMG_SRC = ("'self'", "data:", "https:")Using django-csp:
MIDDLEWARE = [
# ...
'csp.middleware.CSPMiddleware',
]
CSP_DEFAULT_SRC = ("'none'",)
CSP_SCRIPT_SRC = ("'self'", "https://cdn.example.com")
CSP_STYLE_SRC = ("'self'",)
CSP_IMG_SRC = ("'self'", "https://cdn.example.com")
CSP_FONT_SRC = ("'self'", "https://fonts.gstatic.com")Rule: Enable all security headers in production.
---
Performance Optimization
Database Query Optimization
# ✅ PRODUCTION: Monitor slow queries
LOGGING['loggers']['django.db.backends'] = {
'level': 'DEBUG' if DEBUG else 'WARNING',
'handlers': ['console'],
}
# ✅ GOOD: Query timeout (PostgreSQL)
DATABASES['default']['OPTIONS'] = {
'options': '-c statement_timeout=5000' # 5 seconds
}Middleware Optimization
# ✅ Order matters for performance
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware', # Serve static files early
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.cache.UpdateCacheMiddleware', # Cache first
'django.middleware.common.CommonMiddleware',
'django.middleware.cache.FetchFromCacheMiddleware', # Then fetch
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]Compression
# ✅ Enable GZip compression
MIDDLEWARE = [
'django.middleware.gzip.GZipMiddleware', # Add near top
# ... other middleware
]Rule: Always enable compression in production.
---
Environment-Based Settings
Settings Structure
myproject/
├── settings/
│ ├── __init__.py
│ ├── base.py # Shared settings
│ ├── development.py # Local development
│ ├── staging.py # Staging environment
│ └── production.py # Production environmentbase.py:
# Common settings for all environments
INSTALLED_APPS = [...]
MIDDLEWARE = [...]production.py:
from .base import *
DEBUG = False
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(',')
SECURE_SSL_REDIRECT = True
# ... production-specific settingsSet environment:
export DJANGO_SETTINGS_MODULE=myproject.settings.productionUsing Environment Variables
# ✅ RECOMMENDED: python-decouple
from decouple import config, Csv
DEBUG = config('DEBUG', default=False, cast=bool)
SECRET_KEY = config('SECRET_KEY')
ALLOWED_HOSTS = config('ALLOWED_HOSTS', cast=Csv())
DATABASE_URL = config('DATABASE_URL').env file (never commit this):
DEBUG=False
SECRET_KEY=your-secret-key-here
ALLOWED_HOSTS=example.com,www.example.com
DATABASE_URL=postgresql://user:pass@localhost/dbname
REDIS_URL=redis://localhost:6379/1Rule: Store all environment-specific config in environment variables.
---
Deployment Process
Pre-Deployment Steps
# 1. Run tests
python manage.py test
# 2. Check for issues
python manage.py check --deploy
# 3. Check migrations
python manage.py makemigrations --check --dry-run
# 4. Collect static files
python manage.py collectstatic --no-input
# 5. Check for security issues
bandit -r myproject/
safety checkZero-Downtime Deployment
#!/bin/bash
# deploy.sh
# Pull latest code
git pull origin main
# Install dependencies
pip install -r requirements.txt
# Run migrations
python manage.py migrate --no-input
# Collect static files
python manage.py collectstatic --no-input
# Graceful restart (sends SIGHUP to workers)
systemctl reload gunicorn
# Or for manual process management
kill -HUP $(cat /tmp/gunicorn.pid)Health Checks
# myapp/views.py
from django.http import JsonResponse
from django.db import connection
def health_check(request):
"""Simple health check endpoint for load balancers."""
try:
# Check database
with connection.cursor() as cursor:
cursor.execute("SELECT 1")
# Check cache
from django.core.cache import cache
cache.set('health_check', 'ok', 10)
cache.get('health_check')
return JsonResponse({'status': 'healthy'})
except Exception as e:
return JsonResponse({'status': 'unhealthy', 'error': str(e)}, status=500)Rule: Implement health checks for load balancers and monitoring.
---
Monitoring and Observability
Application Performance Monitoring
# ✅ New Relic
import newrelic.agent
newrelic.agent.initialize('/etc/newrelic.ini')
# ✅ DataDog
from ddtrace import patch_all
patch_all()
# ✅ Application Insights (Azure)
from applicationinsights.django import ApplicationInsightsMiddleware
MIDDLEWARE = [
'applicationinsights.django.ApplicationInsightsMiddleware',
# ...
]Key Metrics to Monitor
- Response time: Average, 95th, 99th percentile
- Error rate: 4xx and 5xx responses
- Database query time: Slow query detection
- Cache hit rate: Redis/Memcached efficiency
- Memory usage: Application and database
- CPU usage: Under load
- Disk space: Logs and media files
Rule: Monitor application performance from day one.
---
Backup Strategy
Database Backups
#!/bin/bash
# backup-db.sh
BACKUP_DIR="/backups/postgres"
DATE=$(date +%Y%m%d_%H%M%S)
DB_NAME="mydb"
# Create backup
pg_dump -h localhost -U postgres -Fc $DB_NAME > $BACKUP_DIR/db_$DATE.dump
# Keep only last 7 days
find $BACKUP_DIR -type f -mtime +7 -delete
# Upload to S3
aws s3 cp $BACKUP_DIR/db_$DATE.dump s3://my-backups/postgres/Media Files Backup
#!/bin/bash
# backup-media.sh
MEDIA_DIR="/var/www/myproject/media"
BACKUP_DIR="/backups/media"
DATE=$(date +%Y%m%d)
# Incremental backup
rsync -avz --delete $MEDIA_DIR $BACKUP_DIR/latest/
# Daily snapshot
cp -al $BACKUP_DIR/latest $BACKUP_DIR/$DATERule: Test backup restoration regularly.
---
Checklist Summary
Before going live, verify:
- [ ]
DEBUG = False - [ ]
SECRET_KEYloaded from environment - [ ]
ALLOWED_HOSTSconfigured - [ ] Database backups scheduled
- [ ] Static files collected and served by CDN/nginx
- [ ] HTTPS enabled with proper certificates
- [ ]
SESSION_COOKIE_SECURE = True - [ ]
CSRF_COOKIE_SECURE = True - [ ] Error monitoring configured (Sentry)
- [ ] Logging configured
- [ ] Cache backend configured (Redis)
- [ ] Email backend configured
- [ ] Security headers enabled
- [ ]
python manage.py check --deploypasses - [ ] Health check endpoint implemented
- [ ] Monitoring and alerts configured
- [ ] Deployment process documented
- [ ] Rollback plan prepared
Final command:
python manage.py check --deploy --settings=myproject.settings.productionIf this passes with no warnings, you're ready for production.
Django Expert - Reference Documentation
This directory contains comprehensive Django best practices documentation that will be loaded into Claude's context when needed.
Purpose
Reference files provide detailed guidelines, patterns, and examples that would make SKILL.md too long (>5k words). Claude will read these files as needed based on the specific Django task at hand.
Recommended Reference Files
Create the following markdown files in this directory to provide comprehensive Django guidance:
1. models-and-orm.md - Model Design & ORM Best Practices
Topics to cover:
- Model field types and when to use each
- Database relationships (ForeignKey, ManyToMany, OneToOne)
- Model Meta options (ordering, indexes, constraints)
- Custom model methods and properties
- Model managers and custom querysets
- Database query optimization (select_related, prefetch_related, only, defer)
- Common ORM patterns and anti-patterns
- Migration best practices
- Database indexing strategies
Example structure:
# Django Models and ORM Best Practices
## Model Design Patterns
### Field Choices
- Use TextChoices or IntegerChoices for Django 3.0+
- Keep choices close to the model definition
[detailed examples...]
## Query Optimization
### N+1 Query Problem
[explanation and solutions...]
### Using select_related vs prefetch_related
[detailed guidance with examples...]2. views-and-urls.md - Views, URLs, and Request Handling
Topics to cover:
- Function-based views vs class-based views
- When to use generic CBVs (ListView, DetailView, CreateView, etc.)
- Custom mixins and view composition
- URL patterns and routing best practices
- Request/response handling
- Middleware usage
- Context processors
- Form handling in views
- Error handling and custom error pages
3. drf-guidelines.md - Django REST Framework Best Practices
Topics to cover:
- Serializer patterns (ModelSerializer, nested serializers, write-only fields)
- ViewSets vs APIView vs function-based views
- Permissions and authentication
- Filtering, searching, and pagination
- Versioning strategies
- Custom actions and decorators
- Response formatting
- Error handling in APIs
- Testing DRF endpoints
4. testing-strategies.md - Testing Django Applications
Topics to cover:
- Test organization and structure
- TestCase vs TransactionTestCase
- Factory patterns and fixtures
- Mocking external services
- Testing models, views, and APIs
- Testing permissions and authentication
- Database testing best practices
- Coverage and test performance
- Integration vs unit testing
5. security-checklist.md - Django Security Best Practices
Topics to cover:
- CSRF protection
- XSS prevention
- SQL injection prevention
- Authentication and authorization
- Password handling and storage
- Secure settings for production
- HTTPS and SSL/TLS configuration
- Content Security Policy
- Rate limiting and throttling
- Security headers
- Common vulnerabilities and mitigations
6. performance-optimization.md - Performance and Scaling
Topics to cover:
- Database query optimization
- Caching strategies (per-view cache, template fragment cache, low-level cache)
- Redis/Memcached integration
- Database connection pooling
- Async views and background tasks (Celery)
- Static file optimization
- Database indexing and query profiling
- Monitoring and profiling tools
- Pagination strategies for large datasets
How to Populate These Files
1. Research: Gather Django best practices from official docs, style guides, and community resources 2. Structure: Use clear headings, code examples, and explanations 3. Examples: Include both good and bad examples with explanations 4. Keep Updated: Maintain version-specific guidance (note Django version compatibility) 5. Be Specific: Provide concrete examples rather than general statements
Example Reference File
Here's an example of what a well-structured reference file looks like:
# Django REST Framework Guidelines
## Serializer Best Practices
### 1. Use ModelSerializer for Standard CRUD
For basic model serialization, always start with ModelSerializer:
Good
from rest_framework import serializers
class ProductSerializer(serializers.ModelSerializer): class Meta: model = Product fields = ['id', 'name', 'price', 'category'] read_only_fields = ['id', 'created_at']
### 2. Nested Serializers for Relationships
For related objects, use nested serializers or use depth:
Option 1: Nested serializer (more control)
class OrderSerializer(serializers.ModelSerializer): items = OrderItemSerializer(many=True, read_only=True)
class Meta: model = Order fields = ['id', 'items', 'total']
Option 2: Auto-nesting with depth (simpler but less control)
class OrderSerializer(serializers.ModelSerializer): class Meta: model = Order fields = '__all__' depth = 1 # Be careful with performance
# [Continue with more patterns...]Usage
Claude will automatically read these files when working on Django tasks. You don't need to manually reference them - Claude will determine which references are needed based on the task.
Notes
- Keep files focused on specific topics (don't create one giant file)
- Use code examples liberally
- Include both do's and don'ts
- Reference Django and DRF version numbers when relevant
- Link to official documentation for deep dives
Django Security Checklist
Django is secure by default, but you must configure it correctly and avoid common pitfalls.
OWASP Top 10 & Django
1. SQL Injection
Django protects you automatically when using the ORM.
# ✅ SAFE: ORM parameterizes queries
User.objects.filter(email=user_input)
# ✅ SAFE: Using parameters
User.objects.raw('SELECT * FROM users WHERE email = %s', [user_input])
# ❌ DANGEROUS: String formatting
User.objects.raw(f'SELECT * FROM users WHERE email = "{user_input}"')
# ❌ DANGEROUS: String concatenation
cursor.execute('SELECT * FROM users WHERE id = ' + user_id)Rule: Never use string formatting or concatenation for SQL queries.
2. Cross-Site Scripting (XSS)
Django templates auto-escape by default.
{# ✅ SAFE: Auto-escaped #}
<p>Hello, {{ user.name }}</p>
{# ❌ DANGEROUS: Marks as safe, disables escaping #}
<p>{{ user_bio|safe }}</p>
{# ✅ SAFE: Use linebreaks filter instead #}
<p>{{ user_bio|linebreaks }}</p>In views returning JSON for HTMX:
from django.utils.html import escape
# ✅ GOOD: Escape user input
return JsonResponse({
'message': escape(user_message)
})Rule: Never use |safe or mark_safe() on user-generated content.
3. Cross-Site Request Forgery (CSRF)
Django's CSRF protection is enabled by default.
{# ✅ REQUIRED: Include CSRF token in forms #}
<form method="post">
{% csrf_token %}
...
</form>
{# For HTMX #}
<button hx-post="/api/delete/" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
Delete
</button>In views:
from django.views.decorators.csrf import csrf_exempt, csrf_protect
# ✅ DEFAULT: CSRF protection enabled
@csrf_protect
def my_view(request):
...
# ❌ DANGEROUS: Only exempt for APIs with other auth (tokens)
@csrf_exempt
def api_view(request):
# Must have other protection (API key, JWT, etc.)
...For AJAX/HTMX, include CSRF token in headers:
// HTMX auto-includes if you set this
document.body.addEventListener('htmx:configRequest', (event) => {
event.detail.headers['X-CSRFToken'] = getCookie('csrftoken');
});Rule: Never disable CSRF protection unless you have alternative authentication.
4. Broken Authentication
from django.contrib.auth.decorators import login_required
from rest_framework.permissions import IsAuthenticated
# ✅ GOOD: Require authentication
@login_required
def user_profile(request):
...
class UserProfileView(APIView):
permission_classes = [IsAuthenticated]
...
# ❌ BAD: Checking authentication manually
def user_profile(request):
if 'user_id' in request.session: # Fragile!
...Password Security:
# settings.py
# ✅ GOOD: Strong password validation
AUTH_PASSWORD_VALIDATORS = [
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
'OPTIONS': {'min_length': 12}},
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
]
# ✅ GOOD: Use Argon2 for password hashing
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.Argon2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
]Rule: Always use Django's authentication system, never roll your own.
5. Broken Access Control
# ❌ BAD: No authorization check
@login_required
def delete_post(request, post_id):
post = Post.objects.get(id=post_id)
post.delete() # Any logged-in user can delete any post!
return HttpResponse('Deleted')
# ✅ GOOD: Check authorization
@login_required
def delete_post(request, post_id):
post = get_object_or_404(Post, id=post_id)
# Authorization check
if post.author != request.user and not request.user.is_staff:
return HttpResponseForbidden('You cannot delete this post')
post.delete()
return HttpResponse('Deleted')
# ✅ BETTER: Use permissions
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.author == request.user
class PostDetailView(APIView):
permission_classes = [IsOwnerOrReadOnly]
...Rule: Authentication ≠ Authorization. Always check permissions.
6. Security Misconfiguration
# settings.py
# ❌ DANGEROUS in production
DEBUG = True
SECRET_KEY = 'hardcoded-secret-key'
ALLOWED_HOSTS = ['*']
# ✅ GOOD: Production settings
DEBUG = False
SECRET_KEY = os.environ['DJANGO_SECRET_KEY']
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']
# ✅ GOOD: Security headers
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
SECURE_HSTS_SECONDS = 31536000 # 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# ✅ GOOD: Use environment variables
import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')
DEBUG = os.environ.get('DEBUG', 'False') == 'True'
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(',')
# Database from environment
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ.get('DB_NAME'),
'USER': os.environ.get('DB_USER'),
'PASSWORD': os.environ.get('DB_PASSWORD'),
'HOST': os.environ.get('DB_HOST'),
'PORT': os.environ.get('DB_PORT', '5432'),
}
}Rule: Never commit secrets. Always use environment variables.
7. Sensitive Data Exposure
# ✅ GOOD: Don't log sensitive data
import logging
logger = logging.getLogger(__name__)
def process_payment(payment_data):
# ❌ BAD: Logs credit card number
logger.info(f"Processing payment: {payment_data}")
# ✅ GOOD: Log only safe data
logger.info(f"Processing payment for order {payment_data['order_id']}")
# ✅ GOOD: Don't expose sensitive fields in API
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'email', 'first_name', 'last_name']
# NOT password, social_security_number, etc.
# ✅ GOOD: Redact in admin interface
from django.contrib import admin
class UserAdmin(admin.ModelAdmin):
readonly_fields = ['password'] # Don't allow editing
exclude = ['social_security_number'] # Don't show at allFor database encryption:
# Use django-encrypted-model-fields
pip install django-encrypted-model-fieldsfrom encrypted_model_fields.fields import EncryptedCharField
class PaymentInfo(models.Model):
card_number = EncryptedCharField(max_length=16)Rule: Assume logs, databases, and backups may be compromised. Encrypt sensitive data.
8. Insecure Deserialization
import pickle
import json
# ❌ DANGEROUS: pickle can execute arbitrary code
data = pickle.loads(request.POST['data'])
# ✅ SAFE: Use JSON
data = json.loads(request.POST['data'])
# ✅ SAFE: Use Django's serialization
from django.core import serializers
data = serializers.deserialize('json', request.POST['data'])Rule: Never use pickle or eval() on user input.
9. Using Components with Known Vulnerabilities
# Check for vulnerabilities
pip install safety
safety check
# Keep dependencies updated
pip list --outdatedIn requirements.txt, pin versions:
Django==4.2.7 # Not Django>=4.0 (prevents auto-updates)
psycopg2-binary==2.9.9
redis==5.0.1Rule: Keep dependencies updated and monitor security advisories.
10. Insufficient Logging & Monitoring
import logging
logger = logging.getLogger(__name__)
# ✅ GOOD: Log security events
@login_required
def delete_account(request):
user = request.user
# Log security-relevant action
logger.warning(
f"Account deletion requested",
extra={
'user_id': user.id,
'user_email': user.email,
'ip_address': request.META.get('REMOTE_ADDR'),
'user_agent': request.META.get('HTTP_USER_AGENT'),
}
)
user.delete()
return HttpResponse('Account deleted')
# ✅ GOOD: Log failed authentication
from django.contrib.auth.signals import user_login_failed
def log_failed_login(sender, credentials, request, **kwargs):
logger.warning(
f"Failed login attempt",
extra={
'username': credentials.get('username'),
'ip_address': request.META.get('REMOTE_ADDR'),
}
)
user_login_failed.connect(log_failed_login)Rule: Log security events, failed auth attempts, privilege escalations.
Additional Django Security
Clickjacking Protection
# settings.py
X_FRAME_OPTIONS = 'DENY' # Prevent embedding in iframes
# Or allow specific domains
X_FRAME_OPTIONS = 'SAMEORIGIN' # Only same domainHost Header Validation
# settings.py
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']
# Never use:
ALLOWED_HOSTS = ['*'] # Allows host header injection attacksFile Upload Security
# settings.py
FILE_UPLOAD_MAX_MEMORY_SIZE = 5242880 # 5MB
DATA_UPLOAD_MAX_MEMORY_SIZE = 5242880
# Validate file types
from django.core.exceptions import ValidationError
def validate_file_extension(value):
import os
ext = os.path.splitext(value.name)[1]
valid_extensions = ['.pdf', '.jpg', '.png', '.jpeg']
if ext.lower() not in valid_extensions:
raise ValidationError('Unsupported file extension.')
class Document(models.Model):
file = models.FileField(
upload_to='documents/',
validators=[validate_file_extension]
)
# ✅ GOOD: Store uploads outside web root
MEDIA_ROOT = '/var/www/media/' # Not in static files directory!
MEDIA_URL = '/media/'Rate Limiting
pip install django-ratelimitfrom django_ratelimit.decorators import ratelimit
# Limit login attempts
@ratelimit(key='ip', rate='5/m', block=True)
def login_view(request):
...
# Limit API calls
@ratelimit(key='user', rate='100/h', block=True)
def api_endpoint(request):
...Security Checklist
Before deploying to production:
✅ DEBUG = False ✅ SECRET_KEY from environment variable ✅ ALLOWED_HOSTS configured properly ✅ All security headers enabled (HSTS, CSP, etc.) ✅ HTTPS enforced (SECURE_SSL_REDIRECT = True) ✅ Cookies secure (SESSION_COOKIE_SECURE = True) ✅ CSRF protection enabled (default) ✅ Strong password validators configured ✅ Database credentials in environment variables ✅ File upload size limits set ✅ Rate limiting on authentication endpoints ✅ Logging configured for security events ✅ Dependencies scanned for vulnerabilities ✅ Admin interface protected (strong password, 2FA if possible) ✅ Run python manage.py check --deploy
Useful Django Management Commands
# Check deployment security
python manage.py check --deploy
# This checks for:
# - DEBUG = False
# - SECRET_KEY not hardcoded
# - ALLOWED_HOSTS configured
# - Security middleware enabled
# - And more...---
Remember: Security is not a feature, it's a requirement. Defense in depth - use multiple layers of security.
Django Views & URLs Best Practices
Function-Based Views (FBV) vs Class-Based Views (CBV)
When to Use Each
Use Function-Based Views when:
- Simple, straightforward logic
- Unique behavior that doesn't fit standard CRUD patterns
- You prefer explicit code over implicit behavior
- Handling a single HTTP method
Use Class-Based Views when:
- Standard CRUD operations (list, detail, create, update, delete)
- Sharing behavior across multiple views (mixins)
- Handling multiple HTTP methods on the same endpoint
- You want DRY code with Django's built-in generic views
Function-Based Views
Basic FBV Pattern
from django.shortcuts import render, redirect, get_object_or_404
from django.http import HttpResponse, JsonResponse
from django.contrib.auth.decorators import login_required
# ✅ GOOD: Simple, explicit view
def blog_list(request):
posts = Post.objects.filter(is_published=True).order_by('-created_at')
return render(request, 'blog/list.html', {'posts': posts})
# ✅ GOOD: Handle form submission
def create_post(request):
if request.method == 'POST':
form = PostForm(request.POST)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.save()
return redirect('post_detail', pk=post.pk)
else:
form = PostForm()
return render(request, 'blog/create.html', {'form': form})
# ✅ GOOD: Protected view
@login_required
def user_dashboard(request):
user_posts = Post.objects.filter(author=request.user)
return render(request, 'dashboard.html', {'posts': user_posts})Handling Different HTTP Methods
from django.views.decorators.http import require_http_methods, require_POST
# ✅ GOOD: Restrict to specific methods
@require_http_methods(["GET", "POST"])
def contact_form(request):
if request.method == 'POST':
# Handle form submission
...
return render(request, 'contact.html')
# ✅ GOOD: POST-only view
@require_POST
def delete_post(request, pk):
post = get_object_or_404(Post, pk=pk, author=request.user)
post.delete()
return redirect('blog_list')
# ❌ BAD: No method restriction
def delete_post(request, pk):
# Can be triggered by GET request! (CSRF vulnerability)
post = get_object_or_404(Post, pk=pk)
post.delete()
return redirect('blog_list')Class-Based Views
Generic Class-Based Views
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
# ✅ GOOD: Simple list view
class PostListView(ListView):
model = Post
template_name = 'blog/list.html'
context_object_name = 'posts'
paginate_by = 20
def get_queryset(self):
return Post.objects.filter(is_published=True).select_related('author')
# ✅ GOOD: Detail view with related objects
class PostDetailView(DetailView):
model = Post
template_name = 'blog/detail.html'
context_object_name = 'post'
def get_queryset(self):
return Post.objects.select_related('author').prefetch_related('comments')
# ✅ GOOD: Create view with form validation
class PostCreateView(LoginRequiredMixin, CreateView):
model = Post
form_class = PostForm
template_name = 'blog/create.html'
success_url = reverse_lazy('blog_list')
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
# ✅ GOOD: Update view with permission check
class PostUpdateView(LoginRequiredMixin, UpdateView):
model = Post
form_class = PostForm
template_name = 'blog/edit.html'
def get_queryset(self):
# Only allow editing own posts
return Post.objects.filter(author=self.request.user)
# ✅ GOOD: Delete view
class PostDeleteView(LoginRequiredMixin, DeleteView):
model = Post
success_url = reverse_lazy('blog_list')
def get_queryset(self):
return Post.objects.filter(author=self.request.user)Custom Class-Based Views
from django.views import View
# ✅ GOOD: Handle multiple methods
class PostTogglePublishView(LoginRequiredMixin, View):
def post(self, request, pk):
post = get_object_or_404(Post, pk=pk, author=request.user)
post.is_published = not post.is_published
post.save()
return JsonResponse({
'success': True,
'is_published': post.is_published
})
# ✅ GOOD: API-like view
class PostAPIView(View):
def get(self, request, pk):
post = get_object_or_404(Post, pk=pk)
return JsonResponse({
'id': post.id,
'title': post.title,
'content': post.content,
})
def post(self, request, pk):
post = get_object_or_404(Post, pk=pk, author=request.user)
data = json.loads(request.body)
post.title = data.get('title', post.title)
post.save()
return JsonResponse({'success': True})Mixins
Mixins add reusable functionality to CBVs.
Built-in Mixins
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin, UserPassesTestMixin
# ✅ GOOD: Require login
class UserDashboardView(LoginRequiredMixin, ListView):
model = Post
login_url = '/login/' # Where to redirect if not logged in
def get_queryset(self):
return Post.objects.filter(author=self.request.user)
# ✅ GOOD: Require specific permission
class PostCreateView(PermissionRequiredMixin, CreateView):
model = Post
permission_required = 'blog.add_post'
# Redirects to login if permission denied
# ✅ GOOD: Custom permission check
class PostUpdateView(UserPassesTestMixin, UpdateView):
model = Post
def test_func(self):
post = self.get_object()
return post.author == self.request.user or self.request.user.is_staffCustom Mixins
# ✅ GOOD: Reusable mixin for adding context
class PageTitleMixin:
page_title = ''
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['page_title'] = self.page_title
return context
# ✅ GOOD: Mixin for filtering by author
class FilterByAuthorMixin:
def get_queryset(self):
queryset = super().get_queryset()
return queryset.filter(author=self.request.user)
# Usage
class MyPostsView(FilterByAuthorMixin, PageTitleMixin, ListView):
model = Post
page_title = 'My Posts'
template_name = 'blog/my_posts.html'Rule: Mixins should go before the view class in inheritance order.
URL Configuration
URL Patterns
from django.urls import path, include
from . import views
app_name = 'blog' # ✅ GOOD: Namespace your URLs
urlpatterns = [
# ✅ GOOD: Named URL patterns
path('', views.PostListView.as_view(), name='post_list'),
path('post/<int:pk>/', views.PostDetailView.as_view(), name='post_detail'),
path('post/create/', views.PostCreateView.as_view(), name='post_create'),
path('post/<int:pk>/edit/', views.PostUpdateView.as_view(), name='post_edit'),
path('post/<int:pk>/delete/', views.PostDeleteView.as_view(), name='post_delete'),
# ✅ GOOD: Use path converters
path('post/<slug:slug>/', views.post_detail_by_slug, name='post_by_slug'),
path('category/<str:category>/', views.posts_by_category, name='posts_by_category'),
path('archive/<int:year>/<int:month>/', views.archive, name='archive'),
]URL Converters
Built-in converters:
<int:name>- Matches integers<str:name>- Matches non-empty strings (excluding '/')<slug:name>- Matches slugs (letters, numbers, hyphens, underscores)<uuid:name>- Matches UUIDs<path:name>- Matches any string (including '/')
# Custom converter
class YearConverter:
regex = '[0-9]{4}'
def to_python(self, value):
return int(value)
def to_url(self, value):
return f'{value:04d}'
# Register it
from django.urls import register_converter
register_converter(YearConverter, 'year')
# Use it
path('archive/<year:year>/', views.archive, name='archive')Including Other URLconfs
# project/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('blog/', include('blog.urls')), # ✅ GOOD: Prefix app URLs
path('api/', include('api.urls')),
path('accounts/', include('django.contrib.auth.urls')), # Login/logout
]Reverse URL Resolution
from django.urls import reverse
from django.shortcuts import redirect
# ✅ GOOD: Use reverse() in views
def my_view(request):
return redirect(reverse('blog:post_list'))
# ✅ GOOD: With arguments
def after_create(request, post):
return redirect(reverse('blog:post_detail', kwargs={'pk': post.pk}))
# ✅ GOOD: In templates
# <a href="{% url 'blog:post_detail' pk=post.pk %}">View Post</a>Rule: Never hardcode URLs. Always use reverse() in Python and {% url %} in templates.
Context and Template Rendering
Adding Context to Views
# Function-based view
def blog_list(request):
posts = Post.objects.filter(is_published=True)
categories = Category.objects.all()
context = {
'posts': posts,
'categories': categories,
'page_title': 'Blog Posts',
}
return render(request, 'blog/list.html', context)
# Class-based view
class PostListView(ListView):
model = Post
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['categories'] = Category.objects.all()
context['page_title'] = 'Blog Posts'
return contextContext Processors
For data needed across ALL templates:
# blog/context_processors.py
def blog_stats(request):
return {
'total_posts': Post.objects.count(),
'total_published': Post.objects.filter(is_published=True).count(),
}
# settings.py
TEMPLATES = [
{
'OPTIONS': {
'context_processors': [
...
'blog.context_processors.blog_stats',
],
},
},
]Rule: Only use context processors for truly global data. Don't overuse them.
HTTP Responses
Response Types
from django.http import HttpResponse, JsonResponse, HttpResponseRedirect, Http404
from django.shortcuts import render, redirect
# ✅ GOOD: HTML response
def blog_list(request):
posts = Post.objects.all()
return render(request, 'blog/list.html', {'posts': posts})
# ✅ GOOD: JSON response
def post_api(request, pk):
post = get_object_or_404(Post, pk=pk)
return JsonResponse({
'id': post.id,
'title': post.title,
'content': post.content,
})
# ✅ GOOD: Redirect
def old_url(request):
return redirect('new_url_name')
# ✅ GOOD: 404 error
def post_detail(request, pk):
try:
post = Post.objects.get(pk=pk)
except Post.DoesNotExist:
raise Http404("Post not found")
return render(request, 'blog/detail.html', {'post': post})
# ✅ BETTER: Use get_object_or_404
def post_detail(request, pk):
post = get_object_or_404(Post, pk=pk)
return render(request, 'blog/detail.html', {'post': post})Custom Response Status Codes
from django.http import HttpResponse
# 201 Created
def create_post(request):
post = Post.objects.create(...)
return HttpResponse('Created', status=201)
# 204 No Content
def delete_post(request, pk):
post = get_object_or_404(Post, pk=pk)
post.delete()
return HttpResponse(status=204)
# 400 Bad Request
def api_view(request):
if not request.POST.get('required_field'):
return JsonResponse({'error': 'Missing field'}, status=400)
...
# 403 Forbidden
from django.http import HttpResponseForbidden
def delete_post(request, pk):
post = get_object_or_404(Post, pk=pk)
if post.author != request.user:
return HttpResponseForbidden('You cannot delete this post')
...Error Handling
Custom Error Pages
# views.py
def custom_404(request, exception):
return render(request, '404.html', status=404)
def custom_500(request):
return render(request, '500.html', status=500)
# urls.py
handler404 = 'blog.views.custom_404'
handler500 = 'blog.views.custom_500'Exception Handling in Views
from django.core.exceptions import ValidationError, PermissionDenied
import logging
logger = logging.getLogger(__name__)
# ✅ GOOD: Handle specific exceptions
def process_payment(request):
try:
result = payment_service.charge(request.POST)
return JsonResponse({'success': True})
except ValidationError as e:
return JsonResponse({'error': str(e)}, status=400)
except PermissionDenied:
return JsonResponse({'error': 'Forbidden'}, status=403)
except Exception as e:
logger.exception("Payment processing failed")
return JsonResponse({'error': 'Server error'}, status=500)Middleware
Middleware processes requests/responses globally.
Custom Middleware
# middleware.py
class RequestLoggingMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# Process request before view
logger.info(f"{request.method} {request.path}")
# Call the view
response = self.get_response(request)
# Process response after view
logger.info(f"Response: {response.status_code}")
return response
# settings.py
MIDDLEWARE = [
...
'blog.middleware.RequestLoggingMiddleware',
]Common Middleware Patterns
# Add custom header to all responses
class CustomHeaderMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
response['X-Custom-Header'] = 'value'
return response
# Block requests from specific IPs
class IPBlockMiddleware:
def __init__(self, get_response):
self.get_response = get_response
self.blocked_ips = ['192.168.1.1']
def __call__(self, request):
ip = request.META.get('REMOTE_ADDR')
if ip in self.blocked_ips:
return HttpResponseForbidden('Access denied')
return self.get_response(request)Decorators
Common View Decorators
from django.views.decorators.http import require_http_methods, require_POST
from django.views.decorators.cache import cache_page
from django.contrib.auth.decorators import login_required, permission_required
# ✅ GOOD: Combine decorators
@login_required
@require_POST
def delete_post(request, pk):
...
# ✅ GOOD: Cache view for 5 minutes
@cache_page(60 * 5)
def blog_list(request):
posts = Post.objects.all()
return render(request, 'blog/list.html', {'posts': posts})
# ✅ GOOD: Require permission
@permission_required('blog.add_post')
def create_post(request):
...Custom Decorators
from functools import wraps
# ✅ GOOD: Custom decorator for AJAX-only views
def ajax_required(view_func):
@wraps(view_func)
def wrapper(request, *args, **kwargs):
if not request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return HttpResponseBadRequest('AJAX required')
return view_func(request, *args, **kwargs)
return wrapper
# Usage
@ajax_required
def get_comments(request, post_id):
comments = Comment.objects.filter(post_id=post_id)
return JsonResponse({'comments': list(comments.values())})View Best Practices Checklist
✅ Use get_object_or_404() instead of try/except ✅ Always validate user permissions (authentication ≠ authorization) ✅ Use select_related() and prefetch_related() to avoid N+1 queries ✅ Name all URL patterns for easier maintenance ✅ Use namespaces (app_name) in URL configurations ✅ Validate form data before saving ✅ Return appropriate HTTP status codes ✅ Log errors and security-relevant events ✅ Use CSRF protection on all POST/PUT/DELETE views ✅ Add caching to expensive views when appropriate ✅ Keep views focused (single responsibility) ✅ Extract complex logic into model methods or services
---
Remember: Views should be thin - they orchestrate, they don't contain business logic. Put complex logic in models, managers, or service modules.
Related skills
How it compares
Choose django-expert when building DRF APIs and you need serializer security patterns rather than generic Python or web framework advice.
FAQ
Who is django-expert for?
Developers using agents to execute django expert workflows from SKILL.md.
When should I use django-expert?
Expert Django backend development guidance. Use when creating Django models, views, serializers, or APIs; debugging ORM queries or migrations; optimizing database performance; impl
Is django-expert safe to install?
Review the Security Audits panel on this page before installing in production.