Django REST Framework has matured into the standard for Python API development. Following best practices ensures maintainable, performant APIs. At ZIRA Software, these patterns power our production Django services.
Serializer Best Practices
# Separate read and write serializers
class UserReadSerializer(serializers.ModelSerializer):
posts_count = serializers.SerializerMethodField()
full_name = serializers.SerializerMethodField()
class Meta:
model = User
fields = ['id', 'email', 'full_name', 'posts_count', 'created_at']
def get_posts_count(self, obj):
return obj.posts.count()
def get_full_name(self, obj):
return f"{obj.first_name} {obj.last_name}"
class UserWriteSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True)
class Meta:
model = User
fields = ['email', 'first_name', 'last_name', 'password']
def create(self, validated_data):
password = validated_data.pop('password')
user = User(**validated_data)
user.set_password(password)
user.save()
return user
ViewSet Optimization
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
def get_serializer_class(self):
if self.action in ['create', 'update', 'partial_update']:
return PostWriteSerializer
return PostReadSerializer
def get_queryset(self):
queryset = super().get_queryset()
# Optimize queries based on action
if self.action == 'list':
queryset = queryset.select_related('author').only(
'id', 'title', 'created_at', 'author__id', 'author__name'
)
elif self.action == 'retrieve':
queryset = queryset.select_related('author').prefetch_related(
'comments', 'tags'
)
return queryset
def perform_create(self, serializer):
serializer.save(author=self.request.user)
Pagination
# settings.py
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 20,
}
# Custom pagination
class StandardResultsPagination(pagination.PageNumberPagination):
page_size = 20
page_size_query_param = 'page_size'
max_page_size = 100
def get_paginated_response(self, data):
return Response({
'count': self.page.paginator.count,
'page': self.page.number,
'page_size': self.page_size,
'total_pages': self.page.paginator.num_pages,
'next': self.get_next_link(),
'previous': self.get_previous_link(),
'results': data
})
# Cursor pagination for large datasets
class CursorPagination(pagination.CursorPagination):
page_size = 50
ordering = '-created_at'
Filtering and Search
from django_filters import rest_framework as filters
class PostFilter(filters.FilterSet):
title = filters.CharFilter(lookup_expr='icontains')
author = filters.NumberFilter(field_name='author_id')
created_after = filters.DateFilter(field_name='created_at', lookup_expr='gte')
created_before = filters.DateFilter(field_name='created_at', lookup_expr='lte')
tags = filters.CharFilter(method='filter_tags')
class Meta:
model = Post
fields = ['title', 'author', 'published']
def filter_tags(self, queryset, name, value):
tags = value.split(',')
return queryset.filter(tags__name__in=tags).distinct()
class PostViewSet(viewsets.ModelViewSet):
filterset_class = PostFilter
search_fields = ['title', 'content', 'author__name']
ordering_fields = ['created_at', 'title', 'views']
ordering = ['-created_at']
Error Handling
# Custom exception handler
from rest_framework.views import exception_handler
def custom_exception_handler(exc, context):
response = exception_handler(exc, context)
if response is not None:
response.data['status_code'] = response.status_code
# Standardize error format
if 'detail' in response.data:
response.data['error'] = response.data.pop('detail')
return response
# Custom exceptions
from rest_framework.exceptions import APIException
class ServiceUnavailable(APIException):
status_code = 503
default_detail = 'Service temporarily unavailable.'
default_code = 'service_unavailable'
Versioning
# settings.py
REST_FRAMEWORK = {
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
'DEFAULT_VERSION': 'v1',
'ALLOWED_VERSIONS': ['v1', 'v2'],
}
# urls.py
urlpatterns = [
path('api/<version>/', include('api.urls')),
]
# Version-specific serializers
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = ['id', 'title', 'content']
def to_representation(self, instance):
data = super().to_representation(instance)
if self.context['request'].version == 'v2':
data['author_name'] = instance.author.name
return data
Testing
from rest_framework.test import APITestCase, APIClient
from rest_framework import status
class PostAPITests(APITestCase):
def setUp(self):
self.user = User.objects.create_user(
email='test@example.com',
password='testpass123'
)
self.client = APIClient()
self.client.force_authenticate(user=self.user)
def test_create_post(self):
data = {'title': 'Test Post', 'content': 'Test content'}
response = self.client.post('/api/v1/posts/', data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Post.objects.count(), 1)
self.assertEqual(Post.objects.first().author, self.user)
def test_list_posts_pagination(self):
# Create 25 posts
for i in range(25):
Post.objects.create(title=f'Post {i}', author=self.user)
response = self.client.get('/api/v1/posts/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data['results']), 20)
self.assertIsNotNone(response.data['next'])
Conclusion
These Django REST Framework patterns ensure scalable, maintainable APIs. Proper serializer design, query optimization, and consistent error handling create professional-grade APIs.
Building Django APIs? Contact ZIRA Software for expert development.