Back to Curriculum

Django REST Framework and API Development

📚 Lesson 7 of 8 ⏱️ 70 min

Django REST Framework and API Development

70 min

Django REST Framework (DRF) is a powerful toolkit for building Web APIs in Django. It provides serializers, viewsets, and authentication. DRF extends Django's capabilities specifically for API development, making it easy to build RESTful APIs with features like content negotiation, authentication, permissions, and throttling. It's the most popular way to build APIs with Django.

DRF serializers convert complex data types to Python primitives and vice versa, enabling easy data validation and conversion. Serializers handle the conversion between model instances and JSON, XML, or other content types. They also provide validation, similar to Django forms, ensuring data integrity before it reaches your models. Serializers can be nested for complex relationships.

ViewSets provide a way to combine common CRUD operations into a single class, reducing code duplication and improving maintainability. A ViewSet combines the logic for list, create, retrieve, update, and destroy operations into a single class. Routers automatically generate URL patterns for ViewSets, further reducing boilerplate. This makes API development much faster and more consistent.

Understanding API authentication, permissions, and throttling is crucial for building secure and scalable APIs. DRF provides multiple authentication classes (Token, Session, Basic) and permission classes (IsAuthenticated, IsAdminUser, custom permissions). Throttling controls the rate of requests, preventing abuse and ensuring fair resource usage. These features are essential for production APIs.

DRF includes powerful features like pagination, filtering, and ordering that work seamlessly with ViewSets. Pagination controls how many results are returned per page, filtering allows clients to filter results, and ordering enables sorting. These features are essential for building APIs that handle large datasets efficiently. DRF's browsable API provides a web interface for testing and exploring your API.

API versioning, content negotiation, and custom renderers/parsers enable you to build flexible APIs that can evolve over time. Versioning allows you to maintain multiple API versions simultaneously. Content negotiation enables your API to support multiple formats (JSON, XML, etc.). Custom renderers and parsers allow you to handle custom data formats, making DRF extremely flexible.

Key Concepts

  • DRF provides serializers for data conversion and validation.
  • ViewSets combine CRUD operations into single classes.
  • Routers automatically generate URL patterns for ViewSets.
  • Authentication and permissions control API access.
  • Pagination, filtering, and ordering handle large datasets.

Learning Objectives

Master

  • Creating serializers for model data conversion
  • Using ViewSets and routers for API endpoints
  • Implementing authentication and permissions
  • Adding pagination, filtering, and ordering to APIs

Develop

  • Understanding RESTful API design principles
  • Building secure and scalable APIs
  • Designing API versioning strategies

Tips

  • Use ModelSerializer for simple cases, Serializer for custom logic.
  • Use ViewSets with routers for standard CRUD operations.
  • Implement proper authentication and permissions for security.
  • Use pagination for list endpoints to handle large datasets.

Common Pitfalls

  • Not implementing authentication, exposing APIs to unauthorized access.
  • Not using pagination, causing performance issues with large datasets.
  • Exposing sensitive fields in serializers without proper permissions.
  • Not versioning APIs, making it hard to evolve without breaking clients.

Summary

  • DRF provides powerful tools for building RESTful APIs.
  • Serializers handle data conversion and validation.
  • ViewSets combine CRUD operations reducing boilerplate.
  • Proper authentication and permissions are essential for secure APIs.

Exercise

Implement a comprehensive REST API for the blog application using Django REST Framework with proper serialization, authentication, and permissions.

from rest_framework import serializers, viewsets, permissions, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.pagination import PageNumberPagination
from django_filters import rest_framework as filters
from .models import Post, Category, Comment, Tag
from .permissions import IsAuthorOrReadOnly

# Serializers
class CategorySerializer(serializers.ModelSerializer):
    post_count = serializers.SerializerMethodField()
    
    class Meta:
        model = Category
        fields = ['id', 'name', 'slug', 'description', 'post_count', 'created_at']
        read_only_fields = ['id', 'created_at']
    
    def get_post_count(self, obj):
        return obj.posts.count()

class TagSerializer(serializers.ModelSerializer):
    class Meta:
        model = Tag
        fields = ['id', 'name', 'slug']
        read_only_fields = ['id']

class CommentSerializer(serializers.ModelSerializer):
    author_name = serializers.CharField(source='author_name', read_only=True)
    
    class Meta:
        model = Comment
        fields = ['id', 'post', 'author_name', 'author_email', 'content', 'created_at', 'active']
        read_only_fields = ['id', 'created_at', 'active']
        extra_kwargs = {
            'post': {'write_only': True}
        }

class PostListSerializer(serializers.ModelSerializer):
    author = serializers.StringRelatedField()
    category = serializers.StringRelatedField()
    tags = TagSerializer(many=True, read_only=True)
    comment_count = serializers.SerializerMethodField()
    
    class Meta:
        model = Post
        fields = ['id', 'title', 'slug', 'excerpt', 'author', 'category', 'tags', 'status', 'published_at', 'views', 'comment_count', 'created_at']
        read_only_fields = ['id', 'slug', 'views', 'created_at']
    
    def get_comment_count(self, obj):
        return obj.comments.filter(active=True).count()

class PostDetailSerializer(serializers.ModelSerializer):
    author = serializers.StringRelatedField()
    category = CategorySerializer(read_only=True)
    tags = TagSerializer(many=True, read_only=True)
    comments = CommentSerializer(many=True, read_only=True)
    
    class Meta:
        model = Post
        fields = ['id', 'title', 'slug', 'content', 'excerpt', 'author', 'category', 'tags', 'status', 'featured_image', 'published_at', 'views', 'comments', 'created_at', 'updated_at']
        read_only_fields = ['id', 'slug', 'views', 'created_at', 'updated_at']

class PostCreateSerializer(serializers.ModelSerializer):
    class Meta:
        model = Post
        fields = ['title', 'content', 'excerpt', 'category', 'status', 'featured_image', 'tags']
    
    def validate_title(self, value):
        if len(value) < 5:
            raise serializers.ValidationError('Title must be at least 5 characters long.')
        return value
    
    def validate_content(self, value):
        if len(value) < 50:
            raise serializers.ValidationError('Content must be at least 50 characters long.')
        return value

# Filters
class PostFilter(filters.FilterSet):
    title = filters.CharFilter(lookup_expr='icontains')
    content = filters.CharFilter(lookup_expr='icontains')
    author = filters.CharFilter(field_name='author__username', lookup_expr='icontains')
    category = filters.CharFilter(field_name='category__slug')
    tags = filters.CharFilter(field_name='tags__name', lookup_expr='icontains')
    date_from = filters.DateFilter(field_name='published_at', lookup_expr='gte')
    date_to = filters.DateFilter(field_name='published_at', lookup_expr='lte')
    status = filters.ChoiceFilter(choices=Post.STATUS_CHOICES)
    
    class Meta:
        model = Post
        fields = ['title', 'content', 'author', 'category', 'tags', 'date_from', 'date_to', 'status']

# Pagination
class StandardResultsSetPagination(PageNumberPagination):
    page_size = 10
    page_size_query_param = 'page_size'
    max_page_size = 100

# ViewSets
class CategoryViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = Category.objects.all()
    serializer_class = CategorySerializer
    lookup_field = 'slug'
    pagination_class = StandardResultsSetPagination
    
    @action(detail=True, methods=['get'])
    def posts(self, request, slug=None):
        """Get all posts in a specific category."""
        category = self.get_object()
        posts = Post.objects.filter(category=category, status='published')
        
        # Apply pagination
        page = self.paginate_queryset(posts)
        if page is not None:
            serializer = PostListSerializer(page, many=True)
            return self.get_paginated_response(serializer.data)
        
        serializer = PostListSerializer(posts, many=True)
        return Response(serializer.data)

class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostDetailSerializer
    lookup_field = 'slug'
    pagination_class = StandardResultsSetPagination
    filterset_class = PostFilter
    permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsAuthorOrReadOnly]
    
    def get_queryset(self):
        queryset = Post.objects.select_related('author', 'category').prefetch_related('tags', 'comments')
        if self.action == 'list':
            queryset = queryset.filter(status='published')
        return queryset
    
    def get_serializer_class(self):
        if self.action == 'create':
            return PostCreateSerializer
        elif self.action == 'list':
            return PostListSerializer
        return PostDetailSerializer
    
    def perform_create(self, serializer):
        serializer.save(author=self.request.user)
    
    @action(detail=True, methods=['post'])
    def increment_views(self, request, slug=None):
        """Increment view count for a post."""
        post = self.get_object()
        post.increment_views()
        return Response({'status': 'views incremented'})
    
    @action(detail=True, methods=['get'])
    def comments(self, request, slug=None):
        """Get all comments for a specific post."""
        post = self.get_object()
        comments = post.comments.filter(active=True)
        
        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)
    
    @action(detail=True, methods=['post'])
    def add_comment(self, request, slug=None):
        """Add a comment to a post."""
        post = self.get_object()
        serializer = CommentSerializer(data=request.data)
        
        if serializer.is_valid():
            serializer.save(post=post)
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

class CommentViewSet(viewsets.ModelViewSet):
    queryset = Comment.objects.all()
    serializer_class = CommentSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]
    pagination_class = StandardResultsSetPagination
    
    def get_queryset(self):
        queryset = Comment.objects.select_related('post')
        post_slug = self.request.query_params.get('post', None)
        if post_slug:
            queryset = queryset.filter(post__slug=post_slug)
        return queryset.filter(active=True)
    
    def perform_create(self, serializer):
        serializer.save()

# API URLs
from django.urls import path, include
from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register(r'categories', CategoryViewSet)
router.register(r'posts', PostViewSet)
router.register(r'comments', CommentViewSet)

urlpatterns = [
    path('api/', include(router.urls)),
    path('api/auth/', include('rest_framework.urls')),
]

Exercise Tips

  • Use serializers.SerializerMethodField() for computed fields in serializers.
  • Implement custom permissions: class IsAuthorOrReadOnly(permissions.BasePermission).
  • Use @action decorator for custom ViewSet endpoints beyond CRUD.
  • Enable API versioning: from rest_framework.versioning import NamespaceVersioning.

Code Editor

Output