Mastering Django REST Framework: Patterns for Production APIs
Why Django REST Framework?
After building APIs in FastAPI, Flask, NestJS, and Django, I keep coming back to Django REST Framework (DRF) for projects that need a robust admin panel, ORM, and a rich ecosystem. DRF sits on top of Django and gives you a battle-tested foundation for building APIs.
The Serializer Pattern
Serializers are DRF's core abstraction — they handle both validation and data transformation:
class UserSerializer(serializers.ModelSerializer):
full_name = serializers.SerializerMethodField()
class Meta:
model = User
fields = ["id", "email", "full_name", "created_at"]
read_only_fields = ["id", "created_at"]
def get_full_name(self, obj):
return f"{obj.first_name} {obj.last_name}".strip()
Nested Serializers
One of the most common patterns is nesting related objects:
class PostSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True)
tags = TagSerializer(many=True, read_only=True)
class Meta:
model = Post
fields = ["id", "title", "content", "author", "tags"]
ViewSets: The DRY Way
ViewSets reduce boilerplate by combining CRUD operations into a single class:
class PostViewSet(viewsets.ModelViewSet):
serializer_class = PostSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
filterset_fields = ["status", "author"]
search_fields = ["title", "content"]
ordering_fields = ["created_at", "views"]
def get_queryset(self):
return Post.objects.select_related("author").prefetch_related("tags")
def perform_create(self, serializer):
serializer.save(author=self.request.user)
Performance: The N+1 Problem
The biggest performance killer in Django APIs is the N+1 query problem. Always use select_related for ForeignKey and prefetch_related for ManyToMany:
# ❌ Bad — triggers N+1 queries
posts = Post.objects.all()
for post in posts:
print(post.author.name) # One query per post!
# ✅ Good — single JOIN query
posts = Post.objects.select_related("author").all()
Custom Authentication: JWT
For stateless APIs, JWT authentication is the standard:
from rest_framework_simplejwt.views import TokenObtainPairView
class CustomTokenView(TokenObtainPairView):
serializer_class = CustomTokenSerializer
class CustomTokenSerializer(TokenObtainPairSerializer):
@classmethod
def get_token(cls, user):
token = super().get_token(user)
token["role"] = user.profile.role
token["tenant_id"] = str(user.profile.tenant_id)
return token
Pagination for Large Datasets
Never return unbounded querysets. Use cursor pagination for large tables:
class LargeDatasetPagination(CursorPagination):
page_size = 50
ordering = "-created_at"
cursor_query_param = "cursor"
Conclusion
DRF rewards those who learn its patterns deeply. The combination of serializers, viewsets, and the DRF permission system gives you a clean, maintainable API with very little custom code. The key is understanding when to use each abstraction and always being mindful of database query efficiency.
Start with ModelSerializer and ModelViewSet, add select_related/prefetch_related, and you'll have a solid foundation for most production APIs.