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.
Key Features
- Serialization: Convert Django models to JSON/XML and vice versa
- Class-based Views: Streamlined patterns for common API actions
- Authentication: Support for OAuth, JWT, session auth, and more
- Permissions: Granular control over API access
- Viewsets & Routers: Rapidly build CRUD APIs with minimal code
- Browsable API: Human-friendly HTML interface for API exploration
- Content Negotiation: Support multiple formats (JSON, XML, etc.)
- Throttling: Rate limiting for API requests
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:
- Automatically generated fields based on the model
- Simple default implementations of
create()andupdate() - Automatic validation based on model fields
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.
Views: Processing API Requests
DRF provides several view classes that implement common patterns for handling API requests.
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:
list(): GET to the collectioncreate(): POST to the collectionretrieve(): GET to a single resourceupdate(): PUT to a single resourcepartial_update(): PATCH to a single resourcedestroy(): DELETE to a single resource
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:
^api/books/$- book-list (GET for list, POST for create)^api/books/{pk}/$- book-detail (GET, PUT, PATCH, DELETE)^api/$- API root^api/books/{pk}\.(?P<format>[a-z0-9]+)/?$- book-detail in specified format
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:
detail=True: The action applies to a single object (needs a pk)detail=False: The action applies to the collectionmethods: List of HTTP methods this action responds tourl_path: Custom URL segment (defaults to method name)url_name: Name for the URL (for reverse())permission_classes: Custom permissions for just this action
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:
BasicAuthentication: HTTP Basic Auth (username/password in header)SessionAuthentication: Django's session frameworkTokenAuthentication: Simple token-based authenticationRemoteUserAuthentication: For external auth systems- Third-party: JWT, OAuth, etc.
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:
AllowAny: Anyone can access, regardless of authenticationIsAuthenticated: Only authenticated users can accessIsAdminUser: Only admin users (is_staff=True) can accessIsAuthenticatedOrReadOnly: Authenticated users can perform all operations, unauthenticated users can only perform safe methods (GET, HEAD, OPTIONS)DjangoModelPermissions: Ties into Django's model permissions
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:
?author=1- Filter by author ID (exact match)?published_date__year=2020- Filter by publication year?search=django- Search for "django" in title or author name?ordering=published_date- Order by publication date (ascending)?ordering=-published_date- Order by publication date (descending)
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:
PageNumberPagination: Classic page numbers (e.g.,?page=2)LimitOffsetPagination: Limit and offset parameters (e.g.,?limit=20&offset=40)CursorPagination: Cursor-based pagination for performance with large datasets
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:
Accept: application/jsonAccept: application/xml/api/books.json/api/books.xml
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:
AcceptHeaderVersioning: Version in the Accept header (e.g.,Accept: application/json; version=1.0)URLPathVersioning: Version as part of the URL path (e.g.,/api/v1/books/)NamespaceVersioning: Version in the URL namespace (e.g.,v1:books)QueryParameterVersioning: Version as a query parameter (e.g.,/api/books/?version=v1)HostNameVersioning: Version in the hostname (e.g.,v1.api.example.com)
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:
- Human-readable HTML output for API requests
- Forms for testing POST, PUT, and PATCH requests
- Authentication controls
- Links to related resources
- Documentation from docstrings and serializer fields
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.