Django REST Framework
Master Django REST Framework for building robust, scalable RESTful APIs with proper serialization and authentication.
Serializers
Build type-safe data serialization with Django REST Framework serializers.
from rest_framework import serializers from django.contrib.auth.models import User
class UserSerializer(serializers.ModelSerializer): post_count = serializers.IntegerField(read_only=True) full_name = serializers.SerializerMethodField()
class Meta:
model = User
fields = ['id', 'email', 'name', 'post_count', 'full_name']
read_only_fields = ['id', 'created_at']
extra_kwargs = {
'email': {'required': True},
'password': {'write_only': True}
}
def get_full_name(self, obj):
return f"{obj.first_name} {obj.last_name}"
class PostSerializer(serializers.ModelSerializer): author = UserSerializer(read_only=True) author_id = serializers.IntegerField(write_only=True)
class Meta:
model = Post
fields = '__all__'
def validate_title(self, value):
if len(value) < 5:
raise serializers.ValidationError('Title must be at least 5 characters')
return value
def validate(self, data):
if data.get('published') and not data.get('content'):
raise serializers.ValidationError('Published posts must have content')
return data
def create(self, validated_data):
# Custom creation logic
post = Post.objects.create(**validated_data)
# Send notification, etc.
return post
Custom Fields and Validation
Create custom serializer fields for complex data types.
from rest_framework import serializers
class Base64ImageField(serializers.ImageField): """Handle base64 encoded images."""
def to_internal_value(self, data):
import base64
from django.core.files.base import ContentFile
if isinstance(data, str) and data.startswith('data:image'):
format, imgstr = data.split(';base64,')
ext = format.split('/')[-1]
data = ContentFile(base64.b64decode(imgstr), name=f'temp.{ext}')
return super().to_internal_value(data)
class PostSerializer(serializers.ModelSerializer): image = Base64ImageField(required=False)
class Meta:
model = Post
fields = ['id', 'title', 'image']
Custom validators
def validate_no_profanity(value): profanity_words = ['bad', 'worse'] if any(word in value.lower() for word in profanity_words): raise serializers.ValidationError('Content contains profanity') return value
class CommentSerializer(serializers.ModelSerializer): content = serializers.CharField(validators=[validate_no_profanity])
class Meta:
model = Comment
fields = ['id', 'content', 'created_at']
Nested Serializers
Handle complex nested relationships.
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']
Writable nested serializers
class PostCreateSerializer(serializers.ModelSerializer): comments = CommentSerializer(many=True, required=False)
class Meta:
model = Post
fields = ['id', 'title', 'content', 'comments']
def create(self, validated_data):
comments_data = validated_data.pop('comments', [])
post = Post.objects.create(**validated_data)
for comment_data in comments_data:
Comment.objects.create(post=post, **comment_data)
return post
Dynamic nested serialization
class PostSerializer(serializers.ModelSerializer): class Meta: model = Post fields = ['id', 'title', 'content']
def __init__(self, *args, **kwargs):
include_comments = kwargs.pop('include_comments', False)
super().__init__(*args, **kwargs)
if include_comments:
self.fields['comments'] = CommentSerializer(many=True, read_only=True)
ViewSets
Create RESTful endpoints with ViewSets.
from rest_framework import viewsets, permissions, status from rest_framework.decorators import action from rest_framework.response import Response
class PostViewSet(viewsets.ModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer permission_classes = [permissions.IsAuthenticatedOrReadOnly] filterset_fields = ['author', 'published'] search_fields = ['title', 'content'] ordering_fields = ['created_at', 'title']
def get_queryset(self):
queryset = super().get_queryset()
if self.action == 'list':
queryset = queryset.filter(published=True)
return queryset.select_related('author').prefetch_related('comments')
def get_serializer_class(self):
if self.action == 'create':
return PostCreateSerializer
return PostSerializer
def perform_create(self, serializer):
serializer.save(author=self.request.user)
@action(detail=True, methods=['post'])
def publish(self, request, pk=None):
post = self.get_object()
post.published = True
post.save()
return Response({'status': 'published'})
@action(detail=False, methods=['get'])
def recent(self, request):
recent_posts = self.get_queryset()[:10]
serializer = self.get_serializer(recent_posts, many=True)
return Response(serializer.data)
ReadOnly ViewSet
class CategoryViewSet(viewsets.ReadOnlyModelViewSet): queryset = Category.objects.all() serializer_class = CategorySerializer
Routers
Configure URL routing for ViewSets.
from rest_framework.routers import DefaultRouter, SimpleRouter from django.urls import path, include
Default router (with API root view)
router = DefaultRouter() router.register(r'posts', PostViewSet, basename='post') router.register(r'users', UserViewSet, basename='user') router.register(r'comments', CommentViewSet, basename='comment')
urlpatterns = [ path('api/', include(router.urls)), ]
Simple router (no API root)
simple_router = SimpleRouter() simple_router.register(r'posts', PostViewSet)
Custom routing
from rest_framework.routers import Route, DynamicRoute
class CustomRouter(DefaultRouter): routes = [ Route( url=r'^{prefix}/$', mapping={'get': 'list', 'post': 'create'}, name='{basename}-list', detail=False, initkwargs={} ), # Add custom routes ]
Permissions
Implement authentication and authorization.
from rest_framework import permissions
class IsAuthorOrReadOnly(permissions.BasePermission): """Custom permission to only allow authors to edit."""
def has_object_permission(self, request, view, obj):
# Read permissions for any request
if request.method in permissions.SAFE_METHODS:
return True
# Write permissions only for author
return obj.author == request.user
class IsOwnerOrAdmin(permissions.BasePermission): def has_object_permission(self, request, view, obj): return obj.owner == request.user or request.user.is_staff
Usage in ViewSet
class PostViewSet(viewsets.ModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer permission_classes = [permissions.IsAuthenticated, IsAuthorOrReadOnly]
Multiple permission classes
from rest_framework.permissions import IsAuthenticated, IsAdminUser
class AdminPostViewSet(viewsets.ModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer
def get_permissions(self):
if self.action in ['create', 'update', 'partial_update', 'destroy']:
return [IsAdminUser()]
return [IsAuthenticated()]
Authentication
Configure various authentication methods.
from rest_framework.authentication import TokenAuthentication, SessionAuthentication from rest_framework.authtoken.models import Token from rest_framework.permissions import IsAuthenticated
Token Authentication
class PostViewSet(viewsets.ModelViewSet): authentication_classes = [TokenAuthentication] permission_classes = [IsAuthenticated] queryset = Post.objects.all() serializer_class = PostSerializer
Create token for user
from rest_framework.authtoken.views import obtain_auth_token from django.urls import path
urlpatterns = [ path('api-token-auth/', obtain_auth_token), ]
Custom token authentication
from rest_framework.authtoken.views import ObtainAuthToken from rest_framework.authtoken.models import Token from rest_framework.response import Response
class CustomAuthToken(ObtainAuthToken): def post(self, request, *args, **kwargs): serializer = self.serializer_class(data=request.data, context={'request': request}) serializer.is_valid(raise_exception=True) user = serializer.validated_data['user'] token, created = Token.objects.get_or_create(user=user) return Response({ 'token': token.key, 'user_id': user.pk, 'email': user.email })
JWT Authentication (using djangorestframework-simplejwt)
from rest_framework_simplejwt.authentication import JWTAuthentication
class PostViewSet(viewsets.ModelViewSet): authentication_classes = [JWTAuthentication] permission_classes = [IsAuthenticated]
Filtering and Search
Implement advanced filtering capabilities.
from django_filters import rest_framework as filters from rest_framework import filters as drf_filters
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')
class Meta:
model = Post
fields = ['author', 'published', 'title']
class PostViewSet(viewsets.ModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer filter_backends = [ filters.DjangoFilterBackend, drf_filters.SearchFilter, drf_filters.OrderingFilter ] filterset_class = PostFilter search_fields = ['title', 'content', 'author__name'] ordering_fields = ['created_at', 'title', 'views'] ordering = ['-created_at']
Custom filter backend
class IsOwnerFilterBackend(filters.BaseFilterBackend): def filter_queryset(self, request, queryset, view): return queryset.filter(owner=request.user)
Pagination
Configure pagination for large datasets.
from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination, CursorPagination
class StandardResultsSetPagination(PageNumberPagination): page_size = 10 page_size_query_param = 'page_size' max_page_size = 100
class PostViewSet(viewsets.ModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer pagination_class = StandardResultsSetPagination
Cursor pagination for better performance
class PostCursorPagination(CursorPagination): page_size = 20 ordering = '-created_at'
Custom pagination
class CustomPagination(PageNumberPagination): def get_paginated_response(self, data): return Response({ 'links': { 'next': self.get_next_link(), 'previous': self.get_previous_link() }, 'count': self.page.paginator.count, 'total_pages': self.page.paginator.num_pages, 'results': data })
Throttling
Rate limit API requests.
from rest_framework.throttling import UserRateThrottle, AnonRateThrottle
class BurstRateThrottle(UserRateThrottle): rate = '60/min'
class SustainedRateThrottle(UserRateThrottle): rate = '1000/day'
class PostViewSet(viewsets.ModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer throttle_classes = [BurstRateThrottle, SustainedRateThrottle]
Custom throttle
from rest_framework.throttling import SimpleRateThrottle
class UploadRateThrottle(SimpleRateThrottle): rate = '10/hour'
def get_cache_key(self, request, view):
if request.user.is_authenticated:
ident = request.user.pk
else:
ident = self.get_ident(request)
return self.cache_format % {'scope': self.scope, 'ident': ident}
Versioning
Handle API versioning.
from rest_framework.versioning import URLPathVersioning, NamespaceVersioning
URL path versioning
class PostViewSet(viewsets.ModelViewSet): queryset = Post.objects.all() versioning_class = URLPathVersioning
def get_serializer_class(self):
if self.request.version == 'v1':
return PostSerializerV1
return PostSerializerV2
URLs
urlpatterns = [ path('v1/posts/', PostViewSet.as_view({'get': 'list'})), path('v2/posts/', PostViewSet.as_view({'get': 'list'})), ]
Accept header versioning
from rest_framework.versioning import AcceptHeaderVersioning
REST_FRAMEWORK = { 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning', 'DEFAULT_VERSION': 'v1', 'ALLOWED_VERSIONS': ['v1', 'v2'], }
Error Handling
Implement custom error responses.
from rest_framework.views import exception_handler from rest_framework.response import Response
def custom_exception_handler(exc, context): response = exception_handler(exc, context)
if response is not None:
response.data = {
'error': {
'status_code': response.status_code,
'message': response.data,
'detail': str(exc)
}
}
return response
settings.py
REST_FRAMEWORK = { 'EXCEPTION_HANDLER': 'myapp.utils.custom_exception_handler' }
Custom exceptions
from rest_framework.exceptions import APIException
class ServiceUnavailable(APIException): status_code = 503 default_detail = 'Service temporarily unavailable' default_code = 'service_unavailable'
Usage
from rest_framework import status from rest_framework.response import Response
class PostViewSet(viewsets.ModelViewSet): def create(self, request): try: # Logic pass except Exception as e: raise ServiceUnavailable(detail=str(e))
Advanced Serializer Patterns
Master complex serialization scenarios.
from rest_framework import serializers
Dynamic field selection
class DynamicFieldsModelSerializer(serializers.ModelSerializer): """Serializer that accepts 'fields' parameter to dynamically include/exclude fields."""
def __init__(self, *args, **kwargs):
fields = kwargs.pop('fields', None)
exclude = kwargs.pop('exclude', None)
super().__init__(*args, **kwargs)
if fields is not None:
allowed = set(fields)
existing = set(self.fields)
for field_name in existing - allowed:
self.fields.pop(field_name)
if exclude is not None:
for field_name in exclude:
self.fields.pop(field_name, None)
class PostSerializer(DynamicFieldsModelSerializer): class Meta: model = Post fields = 'all'
Usage:
serializer = PostSerializer(post, fields=('id', 'title', 'author')) serializer = PostSerializer(post, exclude=('content',))
Serializer method field with context
class PostSerializer(serializers.ModelSerializer): is_liked = serializers.SerializerMethodField() like_count = serializers.SerializerMethodField()
class Meta:
model = Post
fields = ['id', 'title', 'is_liked', 'like_count']
def get_is_liked(self, obj):
request = self.context.get('request')
if request and request.user.is_authenticated:
return obj.likes.filter(user=request.user).exists()
return False
def get_like_count(self, obj):
return obj.likes.count()
Nested writable serializers
class CommentSerializer(serializers.ModelSerializer): author_name = serializers.CharField(source='author.name', read_only=True)
class Meta:
model = Comment
fields = ['id', 'content', 'author', 'author_name']
class PostSerializer(serializers.ModelSerializer): comments = CommentSerializer(many=True, required=False)
class Meta:
model = Post
fields = ['id', 'title', 'content', 'comments']
def create(self, validated_data):
comments_data = validated_data.pop('comments', [])
post = Post.objects.create(**validated_data)
for comment_data in comments_data:
Comment.objects.create(post=post, **comment_data)
return post
def update(self, instance, validated_data):
comments_data = validated_data.pop('comments', None)
instance.title = validated_data.get('title', instance.title)
instance.content = validated_data.get('content', instance.content)
instance.save()
if comments_data is not None:
# Clear existing comments
instance.comments.all().delete()
# Create new comments
for comment_data in comments_data:
Comment.objects.create(post=instance, **comment_data)
return instance
Polymorphic serialization
class ContentSerializer(serializers.Serializer): """Base serializer for polymorphic content."""
def to_representation(self, instance):
if isinstance(instance, Article):
return ArticleSerializer(instance, context=self.context).data
elif isinstance(instance, Video):
return VideoSerializer(instance, context=self.context).data
elif isinstance(instance, Image):
return ImageSerializer(instance, context=self.context).data
return super().to_representation(instance)
ViewSet Composition and Actions
Build sophisticated ViewSets with custom actions.
from rest_framework import viewsets, status from rest_framework.decorators import action from rest_framework.response import Response from django.db.models import Count, Q
class PostViewSet(viewsets.ModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer
def get_queryset(self):
queryset = super().get_queryset()
# Filter by query parameters
author = self.request.query_params.get('author')
if author:
queryset = queryset.filter(author_id=author)
published = self.request.query_params.get('published')
if published is not None:
queryset = queryset.filter(published=published == 'true')
# Optimize based on action
if self.action == 'list':
queryset = queryset.select_related('author').only(
'id', 'title', 'created_at', 'author__name'
)
elif self.action == 'retrieve':
queryset = queryset.select_related('author').prefetch_related(
'comments__author', 'tags'
)
return queryset
def get_serializer_class(self):
if self.action == 'list':
return PostListSerializer
elif self.action in ['create', 'update', 'partial_update']:
return PostWriteSerializer
return PostSerializer
@action(detail=True, methods=['post'])
def publish(self, request, pk=None):
"""Publish a post."""
post = self.get_object()
post.published = True
post.published_at = timezone.now()
post.save()
serializer = self.get_serializer(post)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def like(self, request, pk=None):
"""Like a post."""
post = self.get_object()
user = request.user
like, created = Like.objects.get_or_create(post=post, user=user)
if not created:
like.delete()
return Response({'status': 'unliked'})
return Response({'status': 'liked'}, status=status.HTTP_201_CREATED)
@action(detail=False, methods=['get'])
def trending(self, request):
"""Get trending posts."""
posts = self.get_queryset().annotate(
like_count=Count('likes')
).filter(
created_at__gte=timezone.now() - timedelta(days=7)
).order_by('-like_count')[:10]
serializer = self.get_serializer(posts, many=True)
return Response(serializer.data)
@action(detail=False, methods=['get'])
def stats(self, request):
"""Get post statistics."""
queryset = self.get_queryset()
stats = {
'total': queryset.count(),
'published': queryset.filter(published=True).count(),
'drafts': queryset.filter(published=False).count(),
'total_likes': Like.objects.filter(post__in=queryset).count()
}
return Response(stats)
@action(detail=True, methods=['get'])
def comments(self, request, pk=None):
"""Get comments for a post."""
post = self.get_object()
comments = post.comments.select_related('author').all()
page = self.paginate_queryset(comments)
if page is not None:
serializer = CommentSerializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = CommentSerializer(comments, many=True)
return Response(serializer.data)
def perform_create(self, serializer):
"""Save with current user as author."""
serializer.save(author=self.request.user)
def perform_destroy(self, instance):
"""Soft delete instead of hard delete."""
instance.deleted_at = timezone.now()
instance.save()
Advanced Permission Patterns
Implement granular permission control.
from rest_framework import permissions
class IsAuthorOrReadOnly(permissions.BasePermission): """Object-level permission to only allow authors to edit."""
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.author == request.user
class IsPublishedOrAuthor(permissions.BasePermission): """Only show published posts unless user is the author."""
def has_object_permission(self, request, view, obj):
if obj.published:
return True
return obj.author == request.user
class HasAPIKey(permissions.BasePermission): """Check for valid API key in header."""
def has_permission(self, request, view):
api_key = request.META.get('HTTP_X_API_KEY')
if not api_key:
return False
return APIKey.objects.filter(
key=api_key,
is_active=True
).exists()
class RateLimitPermission(permissions.BasePermission): """Custom rate limiting based on user tier."""
def has_permission(self, request, view):
user = request.user
if not user.is_authenticated:
return False
# Check rate limit based on user tier
if user.tier == 'premium':
rate = 1000 # requests per day
else:
rate = 100
# Implement rate limiting logic
cache_key = f'rate_limit_{user.id}'
current_count = cache.get(cache_key, 0)
if current_count >= rate:
return False
cache.set(cache_key, current_count + 1, timeout=86400)
return True
Combine multiple permissions
class PostViewSet(viewsets.ModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer
def get_permissions(self):
if self.action in ['create', 'update', 'partial_update', 'destroy']:
permission_classes = [permissions.IsAuthenticated, IsAuthorOrReadOnly]
elif self.action == 'list':
permission_classes = [permissions.AllowAny]
else:
permission_classes = [IsPublishedOrAuthor]
return [permission() for permission in permission_classes]
Advanced Filtering and Search
Implement sophisticated filtering capabilities.
from django_filters import rest_framework as filters from rest_framework import filters as drf_filters
class PostFilter(filters.FilterSet): # Text filters title = filters.CharFilter(lookup_expr='icontains') title_exact = filters.CharFilter(field_name='title', lookup_expr='exact')
# Date range filters
created_after = filters.DateTimeFilter(field_name='created_at', lookup_expr='gte')
created_before = filters.DateTimeFilter(field_name='created_at', lookup_expr='lte')
# Number range filters
min_views = filters.NumberFilter(field_name='views', lookup_expr='gte')
max_views = filters.NumberFilter(field_name='views', lookup_expr='lte')
# Choice filter
status = filters.ChoiceFilter(choices=(
('published', 'Published'),
('draft', 'Draft'),
('archived', 'Archived')
))
# Multiple choice filter
tags = filters.ModelMultipleChoiceFilter(
queryset=Tag.objects.all(),
field_name='tags',
conjoined=False # OR instead of AND
)
# Custom method filter
has_comments = filters.BooleanFilter(method='filter_has_comments')
class Meta:
model = Post
fields = ['author', 'published', 'category']
def filter_has_comments(self, queryset, name, value):
if value:
return queryset.filter(comments__isnull=False).distinct()
return queryset.filter(comments__isnull=True)
class PostViewSet(viewsets.ModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer filter_backends = [ filters.DjangoFilterBackend, drf_filters.SearchFilter, drf_filters.OrderingFilter ] filterset_class = PostFilter
# Search configuration
search_fields = [
'title',
'content',
'author__name',
'=author__username', # Exact match
'@description', # Full-text search (PostgreSQL)
]
# Ordering configuration
ordering_fields = ['created_at', 'updated_at', 'views', 'title']
ordering = ['-created_at']
Custom filter backend
class IsOwnerFilterBackend(filters.BaseFilterBackend): """Filter objects to show only user's own objects."""
def filter_queryset(self, request, queryset, view):
if not request.user.is_authenticated:
return queryset.none()
return queryset.filter(author=request.user)
class MyPostViewSet(viewsets.ReadOnlyModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer filter_backends = [IsOwnerFilterBackend]
Pagination Strategies
Implement various pagination approaches.
from rest_framework.pagination import ( PageNumberPagination, LimitOffsetPagination, CursorPagination )
class StandardPagination(PageNumberPagination): page_size = 20 page_size_query_param = 'page_size' max_page_size = 100
def get_paginated_response(self, data):
return Response({
'links': {
'next': self.get_next_link(),
'previous': self.get_previous_link()
},
'count': self.page.paginator.count,
'total_pages': self.page.paginator.num_pages,
'current_page': self.page.number,
'results': data
})
class LargeResultsPagination(PageNumberPagination): page_size = 1000 max_page_size = 10000
class SmallResultsPagination(PageNumberPagination): page_size = 10
class PostCursorPagination(CursorPagination): page_size = 20 ordering = '-created_at' cursor_query_param = 'cursor'
def get_paginated_response(self, data):
return Response({
'next': self.get_next_link(),
'previous': self.get_previous_link(),
'results': data
})
class PostViewSet(viewsets.ModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer
def get_pagination_class(self):
if self.action == 'list':
return StandardPagination
elif self.action == 'trending':
return SmallResultsPagination
return None
pagination_class = StandardPagination
API Versioning Strategies
Manage API versions effectively.
from rest_framework.versioning import ( URLPathVersioning, NamespaceVersioning, AcceptHeaderVersioning, QueryParameterVersioning )
URL path versioning
class PostViewSet(viewsets.ModelViewSet): queryset = Post.objects.all() versioning_class = URLPathVersioning
def get_serializer_class(self):
if self.request.version == 'v1':
return PostSerializerV1
elif self.request.version == 'v2':
return PostSerializerV2
return PostSerializer
URLs configuration
urlpatterns = [ path('v1/posts/', PostViewSet.as_view({'get': 'list'}), name='post-list-v1'), path('v2/posts/', PostViewSet.as_view({'get': 'list'}), name='post-list-v2'), ]
Accept header versioning
REST_FRAMEWORK = { 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning', 'DEFAULT_VERSION': 'v1', 'ALLOWED_VERSIONS': ['v1', 'v2', 'v3'], 'VERSION_PARAM': 'version', }
Version-specific serializers
class PostSerializerV1(serializers.ModelSerializer): class Meta: model = Post fields = ['id', 'title', 'content'] # Minimal fields
class PostSerializerV2(serializers.ModelSerializer): author = UserSerializer(read_only=True)
class Meta:
model = Post
fields = ['id', 'title', 'content', 'author', 'created_at']
class PostSerializerV3(serializers.ModelSerializer): author = UserSerializer(read_only=True) comments = CommentSerializer(many=True, read_only=True) tags = TagSerializer(many=True, read_only=True)
class Meta:
model = Post
fields = '__all__'
Testing DRF APIs
Write comprehensive tests for your API.
from rest_framework.test import APITestCase, APIClient, APIRequestFactory from rest_framework import status from django.contrib.auth.models import User from django.urls import reverse
class PostAPITestCase(APITestCase): def setUp(self): self.client = APIClient() self.user = User.objects.create_user('testuser', 'test@test.com', 'testpass') self.client.force_authenticate(user=self.user)
def test_create_post(self):
data = {'title': 'Test 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.get().title, 'Test Post')
def test_list_posts(self):
Post.objects.create(title='Post 1', author=self.user)
Post.objects.create(title='Post 2', author=self.user)
response = self.client.get('/api/posts/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data['results']), 2)
def test_update_post(self):
post = Post.objects.create(title='Old Title', author=self.user)
data = {'title': 'New Title'}
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, 'New Title')
def test_delete_post(self):
post = Post.objects.create(title='Test', author=self.user)
response = self.client.delete(f'/api/posts/{post.id}/')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertEqual(Post.objects.count(), 0)
def test_unauthenticated_access(self):
self.client.force_authenticate(user=None)
response = self.client.post('/api/posts/', {})
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test_permission_denied(self):
other_user = User.objects.create_user('other', password='pass')
post = Post.objects.create(title='Test', author=other_user)
response = self.client.patch(f'/api/posts/{post.id}/', {'title': 'Hacked'})
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_filtering(self):
Post.objects.create(title='Python Post', author=self.user, published=True)
Post.objects.create(title='Django Post', author=self.user, published=False)
response = self.client.get('/api/posts/?published=true')
self.assertEqual(len(response.data['results']), 1)
self.assertEqual(response.data['results'][0]['title'], 'Python Post')
def test_search(self):
Post.objects.create(title='Python Tutorial', author=self.user)
Post.objects.create(title='Django Guide', author=self.user)
response = self.client.get('/api/posts/?search=Python')
self.assertEqual(len(response.data['results']), 1)
def test_ordering(self):
post1 = Post.objects.create(title='A Post', author=self.user)
post2 = Post.objects.create(title='Z Post', author=self.user)
response = self.client.get('/api/posts/?ordering=title')
self.assertEqual(response.data['results'][0]['title'], 'A Post')
response = self.client.get('/api/posts/?ordering=-title')
self.assertEqual(response.data['results'][0]['title'], 'Z Post')
def test_pagination(self):
for i in range(25):
Post.objects.create(title=f'Post {i}', author=self.user)
response = self.client.get('/api/posts/')
self.assertEqual(len(response.data['results']), 20) # Default page size
self.assertIsNotNone(response.data['next'])
def test_custom_action(self):
post = Post.objects.create(title='Test', author=self.user)
response = self.client.post(f'/api/posts/{post.id}/publish/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
post.refresh_from_db()
self.assertTrue(post.published)
Testing with APIRequestFactory
class PostViewSetTestCase(APITestCase): def setUp(self): self.factory = APIRequestFactory() self.user = User.objects.create_user('testuser', password='testpass')
def test_list_action(self):
request = self.factory.get('/api/posts/')
request.user = self.user
view = PostViewSet.as_view({'get': 'list'})
response = view(request)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_create_action(self):
data = {'title': 'Test', 'content': 'Content'}
request = self.factory.post('/api/posts/', data)
request.user = self.user
view = PostViewSet.as_view({'post': 'create'})
response = view(request)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
When to Use This Skill
Use django-rest-framework when building modern, production-ready applications that require advanced patterns, best practices, and optimal performance.
Performance Optimization
Optimize DRF API performance for production.
from django.utils.decorators import method_decorator from django.views.decorators.cache import cache_page from django.views.decorators.vary import vary_on_headers
class PostViewSet(viewsets.ModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer
def get_queryset(self):
queryset = super().get_queryset()
# Optimize based on action
if self.action == 'list':
# Minimal fields for list view
queryset = queryset.select_related('author').only(
'id', 'title', 'created_at', 'author__name'
)
elif self.action == 'retrieve':
# Full data for detail view
queryset = queryset.select_related(
'author', 'category'
).prefetch_related(
'comments__author',
'tags'
)
return queryset
# Cache list view for 5 minutes
@method_decorator(cache_page(60 * 5))
@method_decorator(vary_on_headers('Authorization'))
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
Use only() and defer() in serializers
class PostListSerializer(serializers.ModelSerializer): author_name = serializers.CharField(source='author.name', read_only=True)
class Meta:
model = Post
fields = ['id', 'title', 'author_name', 'created_at']
class PostDetailSerializer(serializers.ModelSerializer): author = UserSerializer(read_only=True) comments = CommentSerializer(many=True, read_only=True)
class Meta:
model = Post
fields = '__all__'
Batch requests
from rest_framework.response import Response from rest_framework import status
class BatchCreateMixin: """Allow batch creation of objects."""
def create(self, request, *args, **kwargs):
many = isinstance(request.data, list)
if not many:
return super().create(request, *args, **kwargs)
serializer = self.get_serializer(data=request.data, many=True)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
return Response(serializer.data, status=status.HTTP_201_CREATED)
class PostViewSet(BatchCreateMixin, viewsets.ModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer
Documentation and Schema
Generate API documentation automatically.
from rest_framework import serializers, viewsets from rest_framework.decorators import action from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiExample from drf_spectacular.types import OpenApiTypes
class PostSerializer(serializers.ModelSerializer): """Serializer for Post objects."""
class Meta:
model = Post
fields = ['id', 'title', 'content', 'author', 'created_at']
read_only_fields = ['id', 'created_at']
class PostViewSet(viewsets.ModelViewSet): """ ViewSet for managing posts.
Provides CRUD operations for posts with additional
custom actions for publishing and liking.
"""
queryset = Post.objects.all()
serializer_class = PostSerializer
@extend_schema(
summary="Publish a post",
description="Set the post's published status to true",
responses={200: PostSerializer}
)
@action(detail=True, methods=['post'])
def publish(self, request, pk=None):
post = self.get_object()
post.published = True
post.save()
serializer = self.get_serializer(post)
return Response(serializer.data)
@extend_schema(
parameters=[
OpenApiParameter(
name='author',
type=OpenApiTypes.INT,
location=OpenApiParameter.QUERY,
description='Filter by author ID'
),
OpenApiParameter(
name='published',
type=OpenApiTypes.BOOL,
location=OpenApiParameter.QUERY,
description='Filter by published status'
)
]
)
def list(self, request, *args, **kwargs):
"""List posts with optional filtering."""
return super().list(request, *args, **kwargs)
settings.py
REST_FRAMEWORK = { 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', }
SPECTACULAR_SETTINGS = { 'TITLE': 'My API', 'DESCRIPTION': 'API for managing posts and comments', 'VERSION': '1.0.0', 'SERVE_INCLUDE_SCHEMA': False, }
urls.py
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
urlpatterns = [ path('api/schema/', SpectacularAPIView.as_view(), name='schema'), path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), ]
DRF Best Practices
-
Use ModelSerializer - Leverage ModelSerializer to reduce boilerplate code
-
Validate at serializer level - Implement validation in serializers, not views
-
Use ViewSets for standard CRUD - ViewSets reduce code duplication for standard operations
-
Optimize with select_related - Always optimize queries in get_queryset()
-
Version your API - Plan for versioning from the start
-
Use proper permissions - Implement granular permissions at object level
-
Implement pagination - Always paginate list endpoints
-
Add throttling - Protect your API with rate limiting
-
Use filtering backends - Enable search and filtering for better UX
-
Write comprehensive tests - Test all endpoints and permission scenarios
-
Cache expensive operations - Use cache decorators for list views
-
Separate read/write serializers - Use different serializers for different actions
-
Document your API - Use drf-spectacular or similar for auto-generated docs
-
Handle errors gracefully - Provide clear error messages for API consumers
-
Use bulk operations - Support batch creation/updates for better performance
DRF Common Pitfalls
-
Not optimizing queries - N+1 problems in serializers accessing related objects
-
Overly complex serializers - Too much logic in serializers instead of models
-
Missing validation - Not validating data at both field and object level
-
Inconsistent API design - Not following REST conventions
-
No pagination - Returning unbounded lists causes performance issues
-
Weak authentication - Not implementing proper token expiration or refresh
-
Missing permissions - Not implementing object-level permissions
-
No API versioning - Breaking changes affect existing clients
-
Poor error messages - Generic errors that don't help API consumers
-
Inadequate testing - Not testing permissions, edge cases, and error scenarios
-
Exposing sensitive data - Returning password hashes or internal IDs
-
Not using read_only_fields - Allowing modification of computed fields
-
Ignoring CORS - Not configuring CORS for frontend applications
-
Missing rate limiting - APIs vulnerable to abuse without throttling
-
Not handling file uploads - Improper handling of multipart/form-data requests
Resources
-
Django REST Framework Documentation
-
DRF Serializers Guide
-
DRF ViewSets
-
DRF Authentication
-
DRF Testing