Django REST Framework: Building Powerful APIs

Complete guide to building RESTful APIs with Django REST Framework including serializers, viewsets, authentication, and advanced features.

S

StalkTechie

Author

February 21, 2025
1000 views

Django REST Framework: Building Powerful APIs

Django REST Framework (DRF) is a powerful and flexible toolkit for building Web APIs in Django. In this comprehensive guide, we'll explore how to build robust, secure, and scalable RESTful APIs using DRF's extensive feature set.

Setting Up Django REST Framework

Installation and Configuration


# Install Django REST Framework
pip install djangorestframework

# Add to INSTALLED_APPS in settings.py
INSTALLED_APPS = [
    ...
    'rest_framework',
    'rest_framework.authtoken',  # For token authentication
]
    

Basic Configuration


# settings.py
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ],
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.TokenAuthentication',
    ],
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 20,
    'DEFAULT_FILTER_BACKENDS': [
        'django_filters.rest_framework.DjangoFilterBackend',
        'rest_framework.filters.SearchFilter',
        'rest_framework.filters.OrderingFilter',
    ],
}
    

Building Your First API

Models


# models.py
from django.db import models
from django.contrib.auth.models import User

class Category(models.Model):
    name = models.CharField(max_length=100)
    slug = models.SlugField(unique=True)
    description = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        verbose_name_plural = "Categories"
        ordering = ['name']
    
    def __str__(self):
        return self.name

class Post(models.Model):
    STATUS_CHOICES = [
        ('draft', 'Draft'),
        ('published', 'Published'),
        ('archived', 'Archived'),
    ]
    
    title = models.CharField(max_length=200)
    slug = models.SlugField(unique=True)
    content = models.TextField()
    excerpt = models.TextField(blank=True)
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
    category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='posts')
    status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
    published_at = models.DateTimeField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    views_count = models.PositiveIntegerField(default=0)
    
    class Meta:
        ordering = ['-published_at', '-created_at']
        indexes = [
            models.Index(fields=['status', 'published_at']),
            models.Index(fields=['author', 'status']),
        ]
    
    def __str__(self):
        return self.title
    
    def save(self, *args, **kwargs):
        if self.status == 'published' and not self.published_at:
            self.published_at = timezone.now()
        super().save(*args, **kwargs)

class Comment(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='comments')
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    approved = models.BooleanField(default=False)
    
    class Meta:
        ordering = ['-created_at']
    
    def __str__(self):
        return f"Comment by {self.author} on {self.post}"
    

Serializers


# serializers.py
from rest_framework import serializers
from django.contrib.auth.models import User
from .models import Category, Post, Comment

class UserSerializer(serializers.ModelSerializer):
    posts_count = serializers.SerializerMethodField()
    
    class Meta:
        model = User
        fields = ['id', 'username', 'email', 'first_name', 'last_name', 'posts_count']
        read_only_fields = ['id', 'posts_count']
    
    def get_posts_count(self, obj):
        return obj.posts.count()

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

class CommentSerializer(serializers.ModelSerializer):
    author = UserSerializer(read_only=True)
    author_id = serializers.PrimaryKeyRelatedField(
        queryset=User.objects.all(), 
        source='author', 
        write_only=True
    )
    
    class Meta:
        model = Comment
        fields = ['id', 'post', 'author', 'author_id', 'content', 'approved', 'created_at', 'updated_at']
        read_only_fields = ['id', 'author', 'created_at', 'updated_at']

class PostListSerializer(serializers.ModelSerializer):
    author = UserSerializer(read_only=True)
    category = CategorySerializer(read_only=True)
    comments_count = serializers.SerializerMethodField()
    excerpt = serializers.SerializerMethodField()
    
    class Meta:
        model = Post
        fields = [
            'id', 'title', 'slug', 'excerpt', 'author', 'category', 
            'status', 'published_at', 'views_count', 'comments_count', 
            'created_at'
        ]
        read_only_fields = ['id', 'author', 'views_count', 'comments_count', 'created_at']
    
    def get_comments_count(self, obj):
        return obj.comments.count()
    
    def get_excerpt(self, obj):
        if obj.excerpt:
            return obj.excerpt
        return obj.content[:150] + '...' if len(obj.content) > 150 else obj.content

class PostDetailSerializer(PostListSerializer):
    comments = CommentSerializer(many=True, read_only=True)
    
    class Meta(PostListSerializer.Meta):
        fields = PostListSerializer.Meta.fields + ['content', 'comments']
    
    def to_representation(self, instance):
        representation = super().to_representation(instance)
        // Increment views count when post is retrieved
        if self.context['request'].method == 'GET':
            instance.views_count += 1
            instance.save(update_fields=['views_count'])
        return representation

class PostCreateSerializer(serializers.ModelSerializer):
    class Meta:
        model = Post
        fields = ['title', 'content', 'excerpt', 'category', 'status']
    
    def validate_title(self, value):
        """
        Custom validation for title
        """
        if len(value) < 5:
            raise serializers.ValidationError("Title must be at least 5 characters long.")
        return value
    
    def create(self, validated_data):
        // Set the author to the current user
        validated_data['author'] = self.context['request'].user
        
        // Generate slug from title
        validated_data['slug'] = slugify(validated_data['title'])
        
        return super().create(validated_data)
    
    def update(self, instance, validated_data):
        // Update slug if title changed
        if 'title' in validated_data and instance.title != validated_data['title']:
            validated_data['slug'] = slugify(validated_data['title'])
        
        return super().update(instance, validated_data)
    

Views and ViewSets

Function-Based Views


# views.py
from rest_framework.decorators import api_view, permission_classes
from rest_framework.response import Response
from rest_framework import status
from django.shortcuts import get_object_or_404
from .models import Post, Comment
from .serializers import PostListSerializer, PostDetailSerializer, CommentSerializer

@api_view(['GET'])
@permission_classes([])  # Allow public access
def post_list(request):
    """
    List all published posts with pagination
    """
    posts = Post.objects.filter(status='published').select_related('author', 'category')
    
    // Search functionality
    search_query = request.query_params.get('search', None)
    if search_query:
        posts = posts.filter(
            Q(title__icontains=search_query) | 
            Q(content__icontains=search_query) |
            Q(excerpt__icontains=search_query)
        )
    
    // Filter by category
    category_slug = request.query_params.get('category', None)
    if category_slug:
        posts = posts.filter(category__slug=category_slug)
    
    // Pagination
    paginator = PageNumberPagination()
    paginator.page_size = 10
    result_page = paginator.paginate_queryset(posts, request)
    
    serializer = PostListSerializer(result_page, many=True, context={'request': request})
    return paginator.get_paginated_response(serializer.data)

@api_view(['GET'])
@permission_classes([])  # Allow public access
def post_detail(request, slug):
    """
    Retrieve a single post by slug
    """
    post = get_object_or_404(
        Post.objects.select_related('author', 'category')
                   .prefetch_related('comments__author'),
        slug=slug,
        status='published'
    )
    
    serializer = PostDetailSerializer(post, context={'request': request})
    return Response(serializer.data)

@api_view(['POST'])
@permission_classes([IsAuthenticated])
def create_comment(request, post_slug):
    """
    Create a new comment on a post
    """
    post = get_object_or_404(Post, slug=post_slug, status='published')
    
    serializer = CommentSerializer(data=request.data, context={'request': request})
    if serializer.is_valid():
        serializer.save(post=post, author=request.user)
        return Response(serializer.data, status=status.HTTP_201_CREATED)
    
    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    

Class-Based Views and ViewSets


# views.py
from rest_framework import viewsets, permissions, filters
from rest_framework.decorators import action
from rest_framework.response import Response
from django_filters.rest_framework import DjangoFilterBackend
from .models import Category, Post, Comment
from .serializers import (
    CategorySerializer, PostListSerializer, 
    PostDetailSerializer, PostCreateSerializer,
    CommentSerializer
)

class CategoryViewSet(viewsets.ReadOnlyModelViewSet):
    """
    ViewSet for viewing categories
    """
    queryset = Category.objects.all()
    serializer_class = CategorySerializer
    permission_classes = [permissions.AllowAny]
    lookup_field = 'slug'
    
    @action(detail=True, methods=['get'])
    def posts(self, request, slug=None):
        """
        Get all posts for a specific category
        """
        category = self.get_object()
        posts = category.posts.filter(status='published').select_related('author')
        
        page = self.paginate_queryset(posts)
        if page is not None:
            serializer = PostListSerializer(page, many=True, context={'request': request})
            return self.get_paginated_response(serializer.data)
        
        serializer = PostListSerializer(posts, many=True, context={'request': request})
        return Response(serializer.data)

class PostViewSet(viewsets.ModelViewSet):
    """
    ViewSet for viewing and editing posts
    """
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]
    filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
    filterset_fields = ['category', 'status', 'author']
    search_fields = ['title', 'content', 'excerpt']
    ordering_fields = ['published_at', 'created_at', 'views_count']
    ordering = ['-published_at']
    lookup_field = 'slug'
    
    def get_queryset(self):
        queryset = Post.objects.all().select_related('author', 'category')
        
        // For non-authenticated users or non-staff, only show published posts
        if not self.request.user.is_authenticated or not self.request.user.is_staff:
            queryset = queryset.filter(status='published')
        
        return queryset
    
    def get_serializer_class(self):
        if self.action == 'list':
            return PostListSerializer
        elif self.action in ['create', 'update', 'partial_update']:
            return PostCreateSerializer
        return PostDetailSerializer
    
    def perform_create(self, serializer):
        serializer.save(author=self.request.user)
    
    @action(detail=True, methods=['post'], permission_classes=[permissions.IsAuthenticated])
    def publish(self, request, slug=None):
        """
        Custom action to publish a post
        """
        post = self.get_object()
        
        if post.author != request.user and not request.user.is_staff:
            return Response(
                {'error': 'You do not have permission to publish this post.'},
                status=status.HTTP_403_FORBIDDEN
            )
        
        post.status = 'published'
        post.published_at = timezone.now()
        post.save()
        
        serializer = self.get_serializer(post)
        return Response(serializer.data)
    
    @action(detail=True, methods=['get'])
    def comments(self, request, slug=None):
        """
        Get all comments for a post
        """
        post = self.get_object()
        comments = post.comments.filter(approved=True).select_related('author')
        
        page = self.paginate_queryset(comments)
        if page is not None:
            serializer = CommentSerializer(page, many=True, context={'request': request})
            return self.get_paginated_response(serializer.data)
        
        serializer = CommentSerializer(comments, many=True, context={'request': request})
        return Response(serializer.data)

class CommentViewSet(viewsets.ModelViewSet):
    """
    ViewSet for viewing and editing comments
    """
    serializer_class = CommentSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]
    
    def get_queryset(self):
        queryset = Comment.objects.all().select_related('author', 'post')
        
        // For non-authenticated users, only show approved comments
        if not self.request.user.is_authenticated:
            queryset = queryset.filter(approved=True)
        
        return queryset
    
    def perform_create(self, serializer):
        serializer.save(author=self.request.user)
    
    @action(detail=True, methods=['post'], permission_classes=[permissions.IsAdminUser])
    def approve(self, request, pk=None):
        """
        Approve a comment (admin only)
        """
        comment = self.get_object()
        comment.approved = True
        comment.save()
        
        serializer = self.get_serializer(comment)
        return Response(serializer.data)
    

Authentication and Permissions

Custom Permissions


# permissions.py
from rest_framework import permissions

class IsAuthorOrReadOnly(permissions.BasePermission):
    """
    Custom permission to only allow authors of an object to edit it.
    """
    
    def has_object_permission(self, request, view, obj):
        // Read permissions are allowed to any request
        if request.method in permissions.SAFE_METHODS:
            return True
        
        // Write permissions are only allowed to the author
        return obj.author == request.user

class IsAdminOrReadOnly(permissions.BasePermission):
    """
    Custom permission to only allow admin users to edit objects.
    """
    
    def has_permission(self, request, view):
        // Read permissions are allowed to any request
        if request.method in permissions.SAFE_METHODS:
            return True
        
        // Write permissions are only allowed to admin users
        return request.user and request.user.is_staff

class IsOwnerOrAdmin(permissions.BasePermission):
    """
    Custom permission to only allow owners or admin to access objects.
    """
    
    def has_object_permission(self, request, view, obj):
        // Admin can do anything
        if request.user and request.user.is_staff:
            return True
        
        // Users can only access their own objects
        return obj == request.user
    

Authentication Setup


# urls.py
from django.urls import path, include
from rest_framework.authtoken.views import obtain_auth_token
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView

urlpatterns = [
    path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
    path('api/auth/', include('rest_framework.urls')),  # Session authentication
]

# views.py - User registration
from django.contrib.auth.models import User
from rest_framework import generics, permissions
from .serializers import UserSerializer

class UserCreateView(generics.CreateAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    permission_classes = [permissions.AllowAny]
    
    def perform_create(self, serializer):
        user = serializer.save()
        user.set_password(serializer.validated_data['password'])
        user.save()

class UserProfileView(generics.RetrieveUpdateAPIView):
    serializer_class = UserSerializer
    permission_classes = [permissions.IsAuthenticated]
    
    def get_object(self):
        return self.request.user
    

Advanced Features

Filtering and Search


# filters.py
import django_filters
from .models import Post

class PostFilter(django_filters.FilterSet):
    title = django_filters.CharFilter(lookup_expr='icontains')
    content = django_filters.CharFilter(lookup_expr='icontains')
    published_after = django_filters.DateFilter(field_name='published_at', lookup_expr='gte')
    published_before = django_filters.DateFilter(field_name='published_at', lookup_expr='lte')
    min_views = django_filters.NumberFilter(field_name='views_count', lookup_expr='gte')
    max_views = django_filters.NumberFilter(field_name='views_count', lookup_expr='lte')
    
    class Meta:
        model = Post
        fields = ['category', 'author', 'status']
    
    @property
    def qs(self):
        parent = super().qs
        return parent.select_related('author', 'category').prefetch_related('comments')

# Using in views
class AdvancedPostViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostListSerializer
    filterset_class = PostFilter
    search_fields = ['title', 'content', 'excerpt', 'author__username']
    ordering_fields = ['published_at', 'created_at', 'views_count', 'title']
    

Throttling and Rate Limiting


# settings.py
REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [
        'rest_framework.throttling.AnonRateThrottle',
        'rest_framework.throttling.UserRateThrottle'
    ],
    'DEFAULT_THROTTLE_RATES': {
        'anon': '100/day',
        'user': '1000/day'
    }
}

# Custom throttling
from rest_framework.throttling import UserRateThrottle

class BurstRateThrottle(UserRateThrottle):
    scope = 'burst'

class SustainedRateThrottle(UserRateThrottle):
    scope = 'sustained'

# In views
class CommentViewSet(viewsets.ModelViewSet):
    throttle_classes = [BurstRateThrottle, SustainedRateThrottle]
    # ... rest of the viewset
    

Testing Your API

API Tests


# tests.py
from django.test import TestCase
from django.urls import reverse
from rest_framework.test import APITestCase, APIClient
from rest_framework import status
from django.contrib.auth.models import User
from .models import Category, Post

class PostAPITestCase(APITestCase):
    
    def setUp(self):
        self.client = APIClient()
        self.user = User.objects.create_user(
            username='testuser',
            email='test@example.com',
            password='testpass123'
        )
        self.admin_user = User.objects.create_superuser(
            username='admin',
            email='admin@example.com',
            password='adminpass123'
        )
        self.category = Category.objects.create(
            name='Technology',
            slug='technology',
            description='Tech related posts'
        )
        self.post = Post.objects.create(
            title='Test Post',
            slug='test-post',
            content='Test content',
            author=self.user,
            category=self.category,
            status='published'
        )
    
    def test_get_post_list(self):
        """
        Test retrieving list of posts
        """
        url = reverse('post-list')
        response = self.client.get(url)
        
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(len(response.data['results']), 1)
        self.assertEqual(response.data['results'][0]['title'], 'Test Post')
    
    def test_create_post_authenticated(self):
        """
        Test creating a post as authenticated user
        """
        self.client.force_authenticate(user=self.user)
        
        url = reverse('post-list')
        data = {
            'title': 'New Post',
            'content': 'New post content',
            'category': self.category.id,
            'status': 'draft'
        }
        
        response = self.client.post(url, data, format='json')
        
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertEqual(Post.objects.count(), 2)
        self.assertEqual(Post.objects.get(slug='new-post').title, 'New Post')
    
    def test_create_post_unauthenticated(self):
        """
        Test that unauthenticated users cannot create posts
        """
        url = reverse('post-list')
        data = {
            'title': 'New Post',
            'content': 'New post content',
            'category': self.category.id
        }
        
        response = self.client.post(url, data, format='json')
        
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
    
    def test_update_own_post(self):
        """
        Test that users can update their own posts
        """
        self.client.force_authenticate(user=self.user)
        
        url = reverse('post-detail', kwargs={'slug': self.post.slug})
        data = {'title': 'Updated Title'}
        
        response = self.client.patch(url, data, format='json')
        
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.post.refresh_from_db()
        self.assertEqual(self.post.title, 'Updated Title')
    
    def test_cannot_update_others_post(self):
        """
        Test that users cannot update posts they don't own
        """
        other_user = User.objects.create_user(
            username='otheruser',
            password='otherpass123'
        )
        self.client.force_authenticate(user=other_user)
        
        url = reverse('post-detail', kwargs={'slug': self.post.slug})
        data = {'title': 'Hacked Title'}
        
        response = self.client.patch(url, data, format='json')
        
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
    

Deployment and Production Considerations

Production Settings


# production.py
from .base import *

DEBUG = False

ALLOWED_HOSTS = ['yourdomain.com', 'api.yourdomain.com']

# Database
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.getenv('DB_NAME'),
        'USER': os.getenv('DB_USER'),
        'PASSWORD': os.getenv('DB_PASSWORD'),
        'HOST': os.getenv('DB_HOST'),
        'PORT': os.getenv('DB_PORT', '5432'),
    }
}

# Security
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True

# CORS settings
CORS_ALLOWED_ORIGINS = [
    "https://yourdomain.com",
    "https://www.yourdomain.com",
]

# REST Framework production settings
REST_FRAMEWORK = {
    'DEFAULT_RENDERER_CLASSES': [
        'rest_framework.renderers.JSONRenderer',
    ],
    'DEFAULT_PARSER_CLASSES': [
        'rest_framework.parsers.JSONParser',
    ],
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.TokenAuthentication',
    ],
    'DEFAULT_THROTTLE_RATES': {
        'anon': '100/day',
        'user': '1000/day'
    }
}
    

Best Practices Summary

  • Use ViewSets for consistent API structure
  • Implement proper authentication and permissions
  • Use serializers for data validation and transformation
  • Implement pagination for large datasets
  • Add filtering, searching, and ordering capabilities
  • Write comprehensive tests for your API endpoints
  • Implement rate limiting for abuse prevention
  • Use proper error handling and status codes
  • Document your API using tools like Swagger/OpenAPI

Django REST Framework provides a powerful foundation for building robust, scalable APIs. By following these patterns and best practices, you can create APIs that are secure, maintainable, and developer-friendly.

Share this post:

Related Articles

Discussion

0 comments

Please log in to join the discussion.

Login to Comment