ð django-rest-framework
Use when Django REST Framework for building APIs with serializers, viewsets, and authentication. Use when creating RESTful APIs.
Overview
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