Django REST Framework Basics

Module 23: Web Frameworks II (Python) - Friday: Lecture 2

Introduction to Django REST Framework

Django REST Framework (DRF) is a powerful toolkit for building Web APIs with Django. It provides a comprehensive set of features that simplify the development of RESTful services while maintaining Django's philosophy of rapid development and clean, pragmatic design.

Real-world analogy: If Django is like a fully-furnished house ready for you to move in, DRF is like adding a professional home office setup that's perfectly designed for a specific purpose (building APIs) while seamlessly matching the existing architecture.

graph TD A[Django] -->|Extends| B[Django REST Framework] B -->|Provides| C[Serialization] B -->|Provides| D[Views & Viewsets] B -->|Provides| E[Authentication] B -->|Provides| F[Permissions] B -->|Provides| G[Content Negotiation] B -->|Provides| H[Browsable API] style A fill:#f9f9f9,stroke:#333,stroke-width:2px style B fill:#e8eaf6,stroke:#3f51b5,stroke-width:2px style C fill:#e3f2fd,stroke:#2196f3,stroke-width:2px style D fill:#e3f2fd,stroke:#2196f3,stroke-width:2px style E fill:#e3f2fd,stroke:#2196f3,stroke-width:2px style F fill:#e3f2fd,stroke:#2196f3,stroke-width:2px style G fill:#e3f2fd,stroke:#2196f3,stroke-width:2px style H fill:#e3f2fd,stroke:#2196f3,stroke-width:2px

Key Features

Installation and Setup


# Install using pip
pip install djangorestframework

# Add to INSTALLED_APPS in settings.py
INSTALLED_APPS = [
    # ...
    'rest_framework',
]

# Optional: Configure default settings
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ],
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication',
    ],
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10
}
            

Serializers: Converting Data

Serializers are the core component of DRF, responsible for converting complex data (like Django models) to native Python datatypes that can be easily rendered into JSON, XML, or other formats.

Real-world analogy: Serializers are like translators that convert between two languages. When your API responds, the serializer translates from "Django-speak" to "JSON-speak" that clients understand. When clients send data, the serializer translates back to "Django-speak".

Basic Serializer


# models.py
from django.db import models

class Book(models.Model):
    title = models.CharField(max_length=100)
    author = models.CharField(max_length=100)
    published_date = models.DateField()
    isbn = models.CharField(max_length=13)
    
    def __str__(self):
        return self.title

# serializers.py
from rest_framework import serializers
from .models import Book

class BookSerializer(serializers.Serializer):
    id = serializers.IntegerField(read_only=True)
    title = serializers.CharField(max_length=100)
    author = serializers.CharField(max_length=100)
    published_date = serializers.DateField()
    isbn = serializers.CharField(max_length=13)
    
    def create(self, validated_data):
        """Create and return a new Book instance"""
        return Book.objects.create(**validated_data)
    
    def update(self, instance, validated_data):
        """Update and return an existing Book instance"""
        instance.title = validated_data.get('title', instance.title)
        instance.author = validated_data.get('author', instance.author)
        instance.published_date = validated_data.get('published_date', instance.published_date)
        instance.isbn = validated_data.get('isbn', instance.isbn)
        instance.save()
        return instance
            

Using the serializer:


# Serializing a single object
book = Book.objects.get(id=1)
serializer = BookSerializer(book)
serializer.data
# {'id': 1, 'title': 'Django for Beginners', 'author': 'William S. Vincent', 
#  'published_date': '2020-01-15', 'isbn': '9781234567897'}

# Serializing multiple objects
books = Book.objects.all()
serializer = BookSerializer(books, many=True)
serializer.data
# [{'id': 1, ...}, {'id': 2, ...}, ...]

# Deserializing (creating an object)
data = {'title': 'Python Crash Course', 'author': 'Eric Matthes', 
       'published_date': '2019-05-03', 'isbn': '9781593279288'}
serializer = BookSerializer(data=data)
if serializer.is_valid():
    book = serializer.save()
else:
    print(serializer.errors)

# Deserializing (updating an object)
book = Book.objects.get(id=1)
data = {'title': 'Updated Title', 'author': 'Same Author', 
       'published_date': '2020-01-15', 'isbn': '9781234567897'}
serializer = BookSerializer(book, data=data, partial=True)
if serializer.is_valid():
    book = serializer.save()
            

ModelSerializer

ModelSerializer is a shortcut that automatically generates serializer fields based on the model:


class BookModelSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = ['id', 'title', 'author', 'published_date', 'isbn']
        # Or include all fields with:
        # fields = '__all__'
        # Or exclude specific fields with:
        # exclude = ['created_at']
            

ModelSerializer automatically provides:

Serializer Relationships

Handling relationships between models is a common task in API development. DRF provides several approaches:

Model Setup


# models.py
class Author(models.Model):
    name = models.CharField(max_length=100)
    biography = models.TextField()
    date_of_birth = models.DateField(null=True, blank=True)
    
    def __str__(self):
        return self.name

class Book(models.Model):
    title = models.CharField(max_length=100)
    author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name='books')
    published_date = models.DateField()
    isbn = models.CharField(max_length=13)
    
    def __str__(self):
        return self.title
            

Nested Serializers


# serializers.py
class AuthorSerializer(serializers.ModelSerializer):
    class Meta:
        model = Author
        fields = ['id', 'name', 'biography', 'date_of_birth']

class BookSerializer(serializers.ModelSerializer):
    # Nested representation of author
    author = AuthorSerializer(read_only=True)
    
    class Meta:
        model = Book
        fields = ['id', 'title', 'author', 'published_date', 'isbn']
        
    # When creating a book, we need to handle the author separately
    def create(self, validated_data):
        author_data = validated_data.pop('author')
        author = Author.objects.create(**author_data)
        book = Book.objects.create(author=author, **validated_data)
        return book
            

PrimaryKeyRelatedField


class BookSerializer(serializers.ModelSerializer):
    # Just include the author's ID
    author = serializers.PrimaryKeyRelatedField(queryset=Author.objects.all())
    
    class Meta:
        model = Book
        fields = ['id', 'title', 'author', 'published_date', 'isbn']
            

StringRelatedField


class BookSerializer(serializers.ModelSerializer):
    # Use the Author's __str__ method
    author = serializers.StringRelatedField()
    
    class Meta:
        model = Book
        fields = ['id', 'title', 'author', 'published_date', 'isbn']
            

SlugRelatedField


class BookSerializer(serializers.ModelSerializer):
    # Use a specific field from the related model
    author = serializers.SlugRelatedField(
        queryset=Author.objects.all(),
        slug_field='name'
    )
    
    class Meta:
        model = Book
        fields = ['id', 'title', 'author', 'published_date', 'isbn']
            

HyperlinkedRelatedField


class BookSerializer(serializers.HyperlinkedModelSerializer):
    # Include a link to the author
    author = serializers.HyperlinkedRelatedField(
        view_name='author-detail',
        queryset=Author.objects.all()
    )
    
    class Meta:
        model = Book
        fields = ['id', 'title', 'author', 'published_date', 'isbn']
            

Reverse Relationships


class AuthorSerializer(serializers.ModelSerializer):
    # Include all books by this author
    books = BookSerializer(many=True, read_only=True)
    
    class Meta:
        model = Author
        fields = ['id', 'name', 'biography', 'date_of_birth', 'books']
            

Advanced Serializer Features

Field-Level Validation


class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = '__all__'
    
    def validate_isbn(self, value):
        """
        Check that the ISBN is valid (simplified example)
        """
        if len(value) != 13 or not value.isdigit():
            raise serializers.ValidationError("ISBN must be a 13-digit number")
        return value
    
    def validate_published_date(self, value):
        """
        Check that the book isn't published in the future
        """
        import datetime
        if value > datetime.date.today():
            raise serializers.ValidationError("Published date cannot be in the future")
        return value
            

Object-Level Validation


def validate(self, data):
    """
    Validate the entire book data
    """
    # Ensure title and author aren't identical
    if data['title'] == data['author'].name:
        raise serializers.ValidationError("Book title cannot be the same as the author name")
    return data
            

Custom Fields


class BookWithFormattedDateSerializer(serializers.ModelSerializer):
    # Format the date nicely
    formatted_date = serializers.SerializerMethodField()
    
    # Calculate a read-only field
    years_since_published = serializers.SerializerMethodField()
    
    class Meta:
        model = Book
        fields = ['id', 'title', 'author', 'published_date', 
                 'formatted_date', 'years_since_published', 'isbn']
    
    def get_formatted_date(self, obj):
        return obj.published_date.strftime("%B %d, %Y")
    
    def get_years_since_published(self, obj):
        import datetime
        today = datetime.date.today()
        delta = today - obj.published_date
        return delta.days // 365
            

Nested Writes with WritableNestedModelSerializer

For complex nested writes, consider using third-party packages like drf-writable-nested:


# pip install drf-writable-nested

from drf_writable_nested import WritableNestedModelSerializer

class AuthorSerializer(serializers.ModelSerializer):
    class Meta:
        model = Author
        fields = ['id', 'name', 'biography']

class BookSerializer(WritableNestedModelSerializer):
    author = AuthorSerializer()
    
    class Meta:
        model = Book
        fields = ['id', 'title', 'author', 'published_date', 'isbn']
            

Now you can create/update both the book and the author in a single request:


data = {
    'title': 'New Book Title',
    'published_date': '2023-01-01',
    'isbn': '9780987654321',
    'author': {
        'name': 'New Author',
        'biography': 'An amazing writer'
    }
}
serializer = BookSerializer(data=data)
if serializer.is_valid():
    book = serializer.save()
            

Views: Processing API Requests

DRF provides several view classes that implement common patterns for handling API requests.

graph TD A[Django View] -->|Extends| B[APIView] B -->|Extends| C[GenericAPIView] C -->|

Views: Processing API Requests

DRF provides several view classes that implement common patterns for handling API requests.

graph TD A[Django View] -->|Extends| B[APIView] B -->|Extends| C[GenericAPIView] C -->|Extends| D[Concrete Generic Views] D -->|Extends| E[ViewSets] style A fill:#f9f9f9,stroke:#333,stroke-width:2px style B fill:#e8eaf6,stroke:#3f51b5,stroke-width:2px style C fill:#e3f2fd,stroke:#2196f3,stroke-width:2px style D fill:#e8f5e9,stroke:#388e3c,stroke-width:2px style E fill:#fff3e0,stroke:#ff9800,stroke-width:2px

Function-Based Views

The simplest approach using decorator functions to enhance regular Django views:


    from rest_framework import status
    from rest_framework.decorators import api_view
    from rest_framework.response import Response
    from .models import Book
    from .serializers import BookSerializer
    
    @api_view(['GET', 'POST'])
    def book_list(request):
        """
        List all books, or create a new book.
        """
        if request.method == 'GET':
            books = Book.objects.all()
            serializer = BookSerializer(books, many=True)
            return Response(serializer.data)
        
        elif request.method == 'POST':
            serializer = BookSerializer(data=request.data)
            if serializer.is_valid():
                serializer.save()
                return Response(serializer.data, status=status.HTTP_201_CREATED)
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    
    @api_view(['GET', 'PUT', 'DELETE'])
    def book_detail(request, pk):
        """
        Retrieve, update or delete a book.
        """
        try:
            book = Book.objects.get(pk=pk)
        except Book.DoesNotExist:
            return Response(status=status.HTTP_404_NOT_FOUND)
        
        if request.method == 'GET':
            serializer = BookSerializer(book)
            return Response(serializer.data)
        
        elif request.method == 'PUT':
            serializer = BookSerializer(book, data=request.data)
            if serializer.is_valid():
                serializer.save()
                return Response(serializer.data)
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
        
        elif request.method == 'DELETE':
            book.delete()
            return Response(status=status.HTTP_204_NO_CONTENT)
                

Class-Based Views: APIView

The base class for all DRF views, providing core functionality:


    from rest_framework.views import APIView
    from rest_framework.response import Response
    from rest_framework import status
    from .models import Book
    from .serializers import BookSerializer
    
    class BookListAPIView(APIView):
        """
        List all books, or create a new book.
        """
        def get(self, request, format=None):
            books = Book.objects.all()
            serializer = BookSerializer(books, many=True)
            return Response(serializer.data)
        
        def post(self, request, format=None):
            serializer = BookSerializer(data=request.data)
            if serializer.is_valid():
                serializer.save()
                return Response(serializer.data, status=status.HTTP_201_CREATED)
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    
    class BookDetailAPIView(APIView):
        """
        Retrieve, update or delete a book instance.
        """
        def get_object(self, pk):
            try:
                return Book.objects.get(pk=pk)
            except Book.DoesNotExist:
                raise Http404
        
        def get(self, request, pk, format=None):
            book = self.get_object(pk)
            serializer = BookSerializer(book)
            return Response(serializer.data)
        
        def put(self, request, pk, format=None):
            book = self.get_object(pk)
            serializer = BookSerializer(book, data=request.data)
            if serializer.is_valid():
                serializer.save()
                return Response(serializer.data)
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
        
        def delete(self, request, pk, format=None):
            book = self.get_object(pk)
            book.delete()
            return Response(status=status.HTTP_204_NO_CONTENT)
                

Generic Views

DRF provides generic views that implement common patterns, reducing boilerplate code:


    from rest_framework import generics
    from .models import Book
    from .serializers import BookSerializer
    
    class BookList(generics.ListCreateAPIView):
        """
        List all books, or create a new book.
        """
        queryset = Book.objects.all()
        serializer_class = BookSerializer
    
    class BookDetail(generics.RetrieveUpdateDestroyAPIView):
        """
        Retrieve, update or delete a book instance.
        """
        queryset = Book.objects.all()
        serializer_class = BookSerializer
                

Here's a breakdown of available generic views:

Generic View Description HTTP Methods
ListAPIView Read-only endpoint for a collection GET
CreateAPIView Endpoint for creating objects POST
RetrieveAPIView Read-only endpoint for a single object GET
UpdateAPIView Endpoint for updating objects PUT, PATCH
DestroyAPIView Endpoint for deleting objects DELETE
ListCreateAPIView Read-write endpoint for a collection GET, POST
RetrieveUpdateAPIView Endpoint for retrieving and updating GET, PUT, PATCH
RetrieveDestroyAPIView Endpoint for retrieving and deleting GET, DELETE
RetrieveUpdateDestroyAPIView Endpoint for retrieving, updating and deleting GET, PUT, PATCH, DELETE

Custom Methods in Generic Views

Generic views can be customized by overriding methods:


    class BookList(generics.ListCreateAPIView):
        queryset = Book.objects.all()
        serializer_class = BookSerializer
        
        # Override to filter queryset based on query parameters
        def get_queryset(self):
            queryset = Book.objects.all()
            title = self.request.query_params.get('title', None)
            if title is not None:
                queryset = queryset.filter(title__icontains=title)
            return queryset
        
        # Override to modify how the serializer is instantiated
        def get_serializer(self, *args, **kwargs):
            # If this is a list request, add context for additional details
            if self.request.method == 'GET' and not kwargs.get('many', False):
                kwargs['context'] = {'detailed': True}
            return super().get_serializer(*args, **kwargs)
        
        # Override to perform additional actions when saving an object
        def perform_create(self, serializer):
            # Add the current user as the creator
            serializer.save(created_by=self.request.user)
                

ViewSets and Routers

ViewSets combine related views into a single class, and Routers automatically generate URL patterns for ViewSets.

Real-world analogy: If views are like individual light switches that control single lights, ViewSets are like home automation panels that control entire rooms, and Routers are like the wiring that connects everything properly without you having to think about it.

Basic ViewSet


    from rest_framework import viewsets
    from .models import Book
    from .serializers import BookSerializer
    
    class BookViewSet(viewsets.ModelViewSet):
        """
        A viewset for viewing and editing books.
        """
        queryset = Book.objects.all()
        serializer_class = BookSerializer
                

This single ViewSet class replaces multiple views, providing:

Routers

Routers automatically generate URL patterns for ViewSets:


    from rest_framework.routers import DefaultRouter
    from .views import BookViewSet
    
    router = DefaultRouter()
    router.register(r'books', BookViewSet)
    
    urlpatterns = [
        # No need to manually define URLs - the router does it for you
        path('api/', include(router.urls)),
    ]
                

This creates the following URL patterns:

Custom Actions in ViewSets

You can add custom actions to ViewSets for operations that don't fit the CRUD model:


    from rest_framework.decorators import action
    from rest_framework.response import Response
    
    class BookViewSet(viewsets.ModelViewSet):
        queryset = Book.objects.all()
        serializer_class = BookSerializer
        
        @action(detail=True, methods=['post'])
        def mark_as_read(self, request, pk=None):
            """
            Custom action to mark a book as read.
            Called via POST /api/books/{pk}/mark_as_read/
            """
            book = self.get_object()
            # Implement the mark as read logic here
            # Example: add the user to a "read by" list
            book.readers.add(request.user)
            return Response({'status': 'book marked as read'})
        
        @action(detail=False, methods=['get'])
        def recent(self, request):
            """
            Custom action to get recent books.
            Called via GET /api/books/recent/
            """
            recent_books = Book.objects.order_by('-published_date')[:5]
            serializer = self.get_serializer(recent_books, many=True)
            return Response(serializer.data)
                

The @action decorator parameters:

Authentication and Permissions

DRF provides flexible authentication and permission systems to control API access.

Authentication Classes

Authentication classes verify the identity of users:


    # Global configuration in settings.py
    REST_FRAMEWORK = {
        'DEFAULT_AUTHENTICATION_CLASSES': [
            'rest_framework.authentication.BasicAuthentication',
            'rest_framework.authentication.SessionAuthentication',
            'rest_framework.authentication.TokenAuthentication',
        ],
    }
    
    # Per-view configuration
    from rest_framework.authentication import TokenAuthentication
    from rest_framework.permissions import IsAuthenticated
    
    class BookViewSet(viewsets.ModelViewSet):
        queryset = Book.objects.all()
        serializer_class = BookSerializer
        authentication_classes = [TokenAuthentication]
        permission_classes = [IsAuthenticated]
                

Common authentication classes:

Token Authentication Setup


    # settings.py
    INSTALLED_APPS = [
        # ...
        'rest_framework.authtoken',
    ]
    
    # After adding to INSTALLED_APPS, run migrations:
    # python manage.py migrate
    
    # views.py
    from rest_framework.authtoken.views import obtain_auth_token
    
    # urls.py
    urlpatterns = [
        # ...
        path('api/token/', obtain_auth_token, name='api_token_auth'),
    ]
                

Clients can then obtain tokens by POSTing credentials:


    POST /api/token/
    {
        "username": "user1",
        "password": "securepassword123"
    }
    
    # Response:
    {
        "token": "9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b"
    }
                

And use the token in subsequent requests:


    GET /api/books/
    Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b
                

Permission Classes

Permission classes determine if a request should be granted access:


    from rest_framework.permissions import IsAuthenticated, IsAdminUser, IsAuthenticatedOrReadOnly
    
    class BookViewSet(viewsets.ModelViewSet):
        queryset = Book.objects.all()
        serializer_class = BookSerializer
        permission_classes = [IsAuthenticatedOrReadOnly]
                

Common permission classes:

Custom Permissions

You can create custom permission classes for more specific rules:


    from rest_framework import permissions
    
    class IsOwnerOrReadOnly(permissions.BasePermission):
        """
        Custom permission to only allow owners 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 owner
            return obj.owner == request.user
    
    class IsAuthorOrReadOnly(permissions.BasePermission):
        """
        Custom permission to only allow authors to edit their books.
        """
        def has_object_permission(self, request, view, obj):
            # Read permissions are allowed to any request
            if request.method in permissions.SAFE_METHODS:
                return True
            
            # Check if the user is the book's author
            return obj.author.user == request.user
                

Using custom permissions:


    class BookViewSet(viewsets.ModelViewSet):
        queryset = Book.objects.all()
        serializer_class = BookSerializer
        permission_classes = [IsAuthenticated, IsAuthorOrReadOnly]
                

Filtering, Searching, and Pagination

DRF provides tools for handling large datasets efficiently.

Filtering with Query Parameters


    class BookViewSet(viewsets.ModelViewSet):
        queryset = Book.objects.all()
        serializer_class = BookSerializer
        
        def get_queryset(self):
            queryset = Book.objects.all()
            
            # Filter by query parameters
            title = self.request.query_params.get('title', None)
            if title:
                queryset = queryset.filter(title__icontains=title)
            
            author = self.request.query_params.get('author', None)
            if author:
                queryset = queryset.filter(author__name__icontains=author)
            
            year = self.request.query_params.get('year', None)
            if year:
                queryset = queryset.filter(published_date__year=year)
            
            return queryset
                

Using Django-Filter

For more complex filtering, the django-filter package is recommended:


    # Install the package
    # pip install django-filter
    
    # settings.py
    INSTALLED_APPS = [
        # ...
        'django_filters',
    ]
    
    REST_FRAMEWORK = {
        # ...
        'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
    }
    
    # views.py
    from django_filters.rest_framework import DjangoFilterBackend
    from rest_framework import filters
    
    class BookViewSet(viewsets.ModelViewSet):
        queryset = Book.objects.all()
        serializer_class = BookSerializer
        filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
        filterset_fields = ['author', 'published_date__year']  # Exact matches
        search_fields = ['title', 'author__name']  # Text search
        ordering_fields = ['title', 'published_date']  # Sorting
                

This setup allows for multiple filter mechanisms:

Custom Filter Set

For more complex filtering logic:


    import django_filters
    from .models import Book
    
    class BookFilter(django_filters.FilterSet):
        min_year = django_filters.NumberFilter(field_name="published_date", lookup_expr='year__gte')
        max_year = django_filters.NumberFilter(field_name="published_date", lookup_expr='year__lte')
        title = django_filters.CharFilter(lookup_expr='icontains')
        author_name = django_filters.CharFilter(field_name='author__name', lookup_expr='icontains')
        
        class Meta:
            model = Book
            fields = ['title', 'author_name', 'min_year', 'max_year']
    
    class BookViewSet(viewsets.ModelViewSet):
        queryset = Book.objects.all()
        serializer_class = BookSerializer
        filter_backends = [DjangoFilterBackend]
        filterset_class = BookFilter
                

Pagination

DRF provides built-in pagination to handle large result sets:


    # Global pagination in settings.py
    REST_FRAMEWORK = {
        'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
        'PAGE_SIZE': 10
    }
    
    # Per-view pagination
    from rest_framework.pagination import PageNumberPagination
    
    class LargeResultsSetPagination(PageNumberPagination):
        page_size = 100
        page_size_query_param = 'page_size'
        max_page_size = 1000
    
    class BookViewSet(viewsets.ModelViewSet):
        queryset = Book.objects.all()
        serializer_class = BookSerializer
        pagination_class = LargeResultsSetPagination
                

DRF provides several pagination styles:

Content Negotiation and Versioning

DRF provides facilities for handling different content formats and API versions.

Content Negotiation

DRF can automatically render responses in different formats based on the client's request:


    # settings.py
    REST_FRAMEWORK = {
        'DEFAULT_RENDERER_CLASSES': [
            'rest_framework.renderers.JSONRenderer',
            'rest_framework.renderers.BrowsableAPIRenderer',
            'rest_framework.renderers.XMLRenderer',
        ],
        'DEFAULT_PARSER_CLASSES': [
            'rest_framework.parsers.JSONParser',
            'rest_framework.parsers.FormParser',
            'rest_framework.parsers.MultiPartParser',
            'rest_framework.parsers.XMLParser',
        ],
    }
                

Clients can request different formats using the Accept header or format suffix:

API Versioning

DRF supports several versioning schemes:


    # settings.py
    REST_FRAMEWORK = {
        'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
        'DEFAULT_VERSION': 'v1',
        'ALLOWED_VERSIONS': ['v1', 'v2'],
        'VERSION_PARAM': 'version',
    }
    
    # urls.py
    urlpatterns = [
        path('api//', include('myapp.urls')),
    ]
    
    # views.py
    class BookViewSet(viewsets.ModelViewSet):
        queryset = Book.objects.all()
        
        def get_serializer_class(self):
            if self.request.version == 'v1':
                return BookSerializerV1
            return BookSerializerV2
                

DRF provides several versioning schemes:

Browsable API

One of DRF's most powerful features is the browsable API, which provides a web interface for exploring and testing your API.

The browsable API offers:

To enable the browsable API:


    # settings.py
    REST_FRAMEWORK = {
        'DEFAULT_RENDERER_CLASSES': [
            'rest_framework.renderers.JSONRenderer',
            'rest_framework.renderers.BrowsableAPIRenderer',
        ],
    }
    
    # urls.py (include auth URLs for login/logout functionality)
    urlpatterns = [
        # ...
        path('api-auth/', include('rest_framework.urls')),
    ]
                

You can customize the browsable API's appearance by overriding DRF templates.

Testing DRF APIs

DRF provides tools to make testing APIs straightforward.


    from django.urls import reverse
    from rest_framework import status
    from rest_framework.test import APITestCase
    from .models import Book, Author
    
    class BookTests(APITestCase):
        def setUp(self):
            # Create test data
            self.author = Author.objects.create(name="Test Author", biography="Test Bio")
            self.book = Book.objects.create(
                title="Test Book",
                author=self.author,
                published_date="2020-01-01",
                isbn="1234567890123"
            )
            self.list_url = reverse('book-list')
            self.detail_url = reverse('book-detail', args=[self.book.id])
        
        def test_get_book_list(self):
            """
            Ensure we can get a list of books.
            """
            response = self.client.get(self.list_url)
            self.assertEqual(response.status_code, status.HTTP_200_OK)
            self.assertEqual(len(response.data['results']), 1)
        
        def test_get_book_detail(self):
            """
            Ensure we can get a single book.
            """
            response = self.client.get(self.detail_url)
            self.assertEqual(response.status_code, status.HTTP_200_OK)
            self.assertEqual(response.data['title'], 'Test Book')
        
        def test_create_book(self):
            """
            Ensure we can create a new book.
            """
            data = {
                'title': 'New Book',
                'author': self.author.id,
                'published_date': '2021-01-01',
                'isbn': '1234567890124'
            }
            response = self.client.post(self.list_url, data, format='json')
            self.assertEqual(response.status_code, status.HTTP_201_CREATED)
            self.assertEqual(Book.objects.count(), 2)
            self.assertEqual(Book.objects.get(id=response.data['id']).title, 'New Book')
        
        def test_update_book(self):
            """
            Ensure we can update a book.
            """
            data = {
                'title': 'Updated Book',
                'author': self.author.id,
                'published_date': '2020-01-01',
                'isbn': '1234567890123'
            }
            response = self.client.put(self.detail_url, data, format='json')
            self.assertEqual(response.status_code, status.HTTP_200_OK)
            self.assertEqual(Book.objects.get(id=self.book.id).title, 'Updated Book')
        
        def test_delete_book(self):
            """
            Ensure we can delete a book.
            """
            response = self.client.delete(self.detail_url)
            self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
            self.assertEqual(Book.objects.count(), 0)
                

Testing with Authentication


    from django.contrib.auth.models import User
    from rest_framework.authtoken.models import Token
    
    class AuthenticatedBookTests(APITestCase):
        def setUp(self):
            # Create a user
            self.user = User.objects.create_user(
                username='testuser',
                email='test@example.com',
                password='testpassword'
            )
            # Create a token
            self.token = Token.objects.create(user=self.user)
            # Include the token in all requests
            self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token.key)
            
            # Set up book data
            self.author = Author.objects.create(name="Test Author")
            # ... rest of setup
        
        def test_authenticated_request(self):
            """
            Ensure authenticated requests work.
            """
            response = self.client.get(self.list_url)
            self.assertEqual(response.status_code, status.HTTP_200_OK)
        
        def test_unauthenticated_request(self):
            """
            Ensure unauthenticated requests are denied.
            """
            # Remove credentials
            self.client.credentials()
            response = self.client.post(self.list_url, {'title': 'Test'})
            self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
                

Practical Example: Complete REST API

Let's put everything together in a complete example of a book management API:

Models


    # models.py
    from django.db import models
    from django.contrib.auth.models import User
    
    class Author(models.Model):
        name = models.CharField(max_length=100)
        biography = models.TextField(blank=True)
        date_of_birth = models.DateField(null=True, blank=True)
        
        def __str__(self):
            return self.name
    
    class Genre(models.Model):
        name = models.CharField(max_length=100)
        
        def __str__(self):
            return self.name
    
    class Book(models.Model):
        title = models.CharField(max_length=100)
        author = models.ForeignKey(Author, related_name='books', on_delete=models.CASCADE)
        published_date = models.DateField()
        isbn = models.CharField(max_length=13)
        genres = models.ManyToManyField(Genre, related_name='books')
        description = models.TextField(blank=True)
        created_by = models.ForeignKey(User, related_name='books', on_delete=models.SET_NULL, null=True)
        created_at = models.DateTimeField(auto_now_add=True)
        updated_at = models.DateTimeField(auto_now=True)
        
        def __str__(self):
            return self.title
    
    class Review(models.Model):
        book = models.ForeignKey(Book, related_name='reviews', on_delete=models.CASCADE)
        user = models.ForeignKey(User, related_name='reviews', on_delete=models.CASCADE)
        rating = models.IntegerField()
        comment = models.TextField(blank=True)
        created_at = models.DateTimeField(auto_now_add=True)
        
        class Meta:
            unique_together = ['book', 'user']  # One review per user per book
        
        def __str__(self):
            return f"{self.user.username}'s review of {self.book.title}"
                

Serializers


    # serializers.py
    from rest_framework import serializers
    from .models import Author, Genre, Book, Review
    from django.contrib.auth.models import User
    
    class UserSerializer(serializers.ModelSerializer):
        class Meta:
            model = User
            fields = ['id', 'username', 'email']
    
    class GenreSerializer(serializers.ModelSerializer):
        class Meta:
            model = Genre
            fields = ['id', 'name']
    
    class AuthorSerializer(serializers.ModelSerializer):
        class Meta:
            model = Author
            fields = ['id', 'name', 'biography', 'date_of_birth']
    
    class ReviewSerializer(serializers.ModelSerializer):
        user = UserSerializer(read_only=True)
        
        class Meta:
            model = Review
            fields = ['id', 'book', 'user', 'rating', 'comment', 'created_at']
            read_only_fields = ['user']
        
        def create(self, validated_data):
            validated_data['user'] = self.context['request'].user
            return super().create(validated_data)
    
    class BookSerializer(serializers.ModelSerializer):
        author = AuthorSerializer(read_only=True)
        author_id = serializers.PrimaryKeyRelatedField(
            queryset=Author.objects.all(),
            write_only=True,
            source='author'
        )
        genres = GenreSerializer(many=True, read_only=True)
        genre_ids = serializers.PrimaryKeyRelatedField(
            queryset=Genre.objects.all(),
            write_only=True,
            source='genres',
            many=True
        )
        reviews = ReviewSerializer(many=True, read_only=True)
        created_by = UserSerializer(read_only=True)
        average_rating = serializers.SerializerMethodField()
        
        class Meta:
            model = Book
            fields = [
                'id', 'title', 'author', 'author_id', 'published_date', 
                'isbn', 'genres', 'genre_ids', 'description', 'created_by',
                'created_at', 'updated_at', 'reviews', 'average_rating'
            ]
            read_only_fields = ['created_by', 'created_at', 'updated_at']
        
        def get_average_rating(self, obj):
            reviews = obj.reviews.all()
            if reviews:
                return sum(review.rating for review in reviews) / len(reviews)
            return None
        
        def create(self, validated_data):
            genres = validated_data.pop('genres')
            validated_data['created_by'] = self.context['request'].user
            book = Book.objects.create(**validated_data)
            book.genres.set(genres)
            return book
                

Views and Permissions


    # permissions.py
    from rest_framework import permissions
    
    class IsAdminOrReadOnly(permissions.BasePermission):
        """
        Allow read access to everyone, but only write access to admins.
        """
        def has_permission(self, request, view):
            if request.method in permissions.SAFE_METHODS:
                return True
            return request.user and request.user.is_staff
    
    class IsCreatorOrReadOnly(permissions.BasePermission):
        """
        Allow read access to everyone, but only write access to creator.
        """
        def has_object_permission(self, request, view, obj):
            if request.method in permissions.SAFE_METHODS:
                return True
            return obj.created_by == request.user
    
    class IsReviewCreatorOrReadOnly(permissions.BasePermission):
        """
        Allow read access to everyone, but only write access to the review creator.
        """
        def has_object_permission(self, request, view, obj):
            if request.method in permissions.SAFE_METHODS:
                return True
            return obj.user == request.user
    
    # views.py
    from rest_framework import viewsets, permissions, filters
    from django_filters.rest_framework import DjangoFilterBackend
    from .models import Author, Genre, Book, Review
    from .serializers import AuthorSerializer, GenreSerializer, BookSerializer, ReviewSerializer
    from .permissions import IsAdminOrReadOnly, IsCreatorOrReadOnly, IsReviewCreatorOrReadOnly
    
    class AuthorViewSet(viewsets.ModelViewSet):
        queryset = Author.objects.all()
        serializer_class = AuthorSerializer
        permission_classes = [IsAdminOrReadOnly]
        filter_backends = [filters.SearchFilter, filters.OrderingFilter]
        search_fields = ['name']
        ordering_fields = ['name', 'date_of_birth']
    
    class GenreViewSet(viewsets.ModelViewSet):
        queryset = Genre.objects.all()
        serializer_class = GenreSerializer
        permission_classes = [IsAdminOrReadOnly]
        filter_backends = [filters.SearchFilter]
        search_fields = ['name']
    
    class BookViewSet(viewsets.ModelViewSet):
        queryset = Book.objects.all()
        serializer_class = BookSerializer
        permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsCreatorOrReadOnly]
        filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
        filterset_fields = ['author', 'genres', 'published_date__year']
        search_fields = ['title', 'author__name', 'description']
        ordering_fields = ['title', 'published_date', 'created_at']
        
        def get_queryset(self):
            queryset = super().get_queryset()
            # Example of custom filtering
            if self.request.query_params.get('created_by_me'):
                queryset = queryset.filter(created_by=self.request.user)
            return queryset
    
    class ReviewViewSet(viewsets.ModelViewSet):
        queryset = Review.objects.all()
        serializer_class = ReviewSerializer
        permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsReviewCreatorOrReadOnly]
        filter_backends = [DjangoFilterBackend, filters.OrderingFilter]
        filterset_fields = ['book', 'user', 'rating']
        ordering_fields = ['created_at', 'rating']
                

URLs and Router


    # urls.py
    from django.urls import path, include
    from rest_framework.routers import DefaultRouter
    from .views import AuthorViewSet, GenreViewSet, BookViewSet, ReviewViewSet
    
    router = DefaultRouter()
    router.register(r'authors', AuthorViewSet)
    router.register(r'genres', GenreViewSet)
    router.register(r'books', BookViewSet)
    router.register(r'reviews', ReviewViewSet)
    
    urlpatterns = [
        path('api/', include(router.urls)),
        path('api-auth/', include('rest_framework.urls')),
    ]
                

Practice Activities

Basic Exercise: Book API

Create a simple Django REST Framework API for a collection of books. Include serializers, views using generic views, and URL configuration. Implement basic filtering and searching capabilities.

Intermediate Exercise: Blog API

Build a blog API with posts, categories, and comments. Implement authentication, permissions, and relationships between models. Use ViewSets and routers for URL configuration.

Advanced Exercise: E-Commerce API

Create an e-commerce API with products, categories, users, orders, and reviews. Implement complex filtering, pagination, authentication, and permissions. Add custom actions to ViewSets for operations like "add to cart" and "checkout".

Challenge: API with Multiple Serializers

Extend any of the above exercises to include multiple serialization formats for different purposes (e.g., list view vs. detail view) and versioning. Add nested serializers for related objects and custom validation rules.

Further Resources