Back to Blog
DjangoPythonREST APIPostgreSQLBackend

Mastering Django REST Framework: Patterns for Production APIs

2024-08-20·10 min read

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.