| name | django-rest-framework |
| description | Use when Django REST Framework for building APIs with serializers, viewsets, and authentication. Use when creating RESTful APIs. |
| allowed-tools | Bash, Read |
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