
Building a REST API with Django REST Framework
Django REST Framework is a powerful and flexible toolkit for building Web APIs
Introduction
Django REST Framework (DRF) is a powerful, flexible toolkit for building Web APIs in Python. Built on top of Django, it provides a comprehensive set of tools for serialization, authentication, permissions, throttling, and API browsing. This guide will walk you through building a production-ready REST API from scratch, covering fundamental concepts and best practices.
Understanding REST APIs
REST (Representational State Transfer) is an architectural style for designing networked applications. REST APIs use HTTP requests to perform CRUD (Create, Read, Update, Delete) operations on resources, making them language-agnostic and platform-independent.
Core REST Principles
Resource-Based Architecture
In REST, everything is a resource identified by a URI. Resources are nouns (e.g., /users, /todos) rather than verbs. Each resource can have multiple representations (JSON, XML, HTML).
Statelessness
Each request from client to server must contain all information needed to understand and process the request. The server stores no client context between requests, making the API scalable and reliable.
Standard HTTP Methods
REST APIs leverage HTTP methods with specific semantics:
- GET: Retrieve a resource or collection (safe, idempotent)
- POST: Create a new resource (not idempotent)
- PUT: Update a resource by replacing it entirely (idempotent)
- PATCH: Partially update a resource (idempotent)
- DELETE: Remove a resource (idempotent)
HTTP Status Codes
Proper status codes communicate the result of operations:
- 200 OK: Successful GET, PUT, or PATCH
- 201 Created: Successful POST
- 204 No Content: Successful DELETE
- 400 Bad Request: Invalid request data
- 401 Unauthorized: Authentication required
- 403 Forbidden: Authenticated but not authorized
- 404 Not Found: Resource doesn't exist
- 500 Internal Server Error: Server-side error
HATEOAS (Hypermedia as the Engine of Application State)
Responses include links to related resources, allowing clients to discover available actions dynamically.
REST API Design Best Practices
Consistent Naming Conventions
Use plural nouns for collections (/todos not /todo), lowercase letters, hyphens for multi-word resources (/user-profiles), and nest resources logically (/users/1/todos).
Versioning
Version your API to manage breaking changes:
/api/v1/todos/
/api/v2/todos/
Filtering, Sorting, and Pagination
Support query parameters for flexible data retrieval:
GET /api/v1/todos/?completed=true&ordering=-created_at&page=2
Meaningful Error Messages
Return structured error responses:
{
"error": "Validation failed",
"details": {
"title": ["This field is required."]
}
}
Why Django REST Framework?
Django provides an excellent foundation for web applications, and DRF extends it specifically for API development.
Django's Strengths
Robust ORM
Django's Object-Relational Mapper abstracts database operations, supporting PostgreSQL, MySQL, SQLite, and Oracle. The ORM handles migrations, relationships, and complex queries.
Security Features
Django includes protection against SQL injection, XSS, CSRF, clickjacking, and provides secure password hashing out of the box.
Scalability
Django's shared-nothing architecture and caching framework support horizontal scaling.
Admin Interface
The auto-generated admin interface accelerates development by providing immediate CRUD functionality for data management.
Extensive Ecosystem
Django's mature ecosystem includes packages for almost any requirement, from payment processing to full-text search.
DRF's Additional Features
Powerful Serialization
DRF serializers handle complex data type conversions, validation, and nested relationships. They work bidirectionally, serializing Python objects to JSON and deserializing JSON to Python objects.
Class-Based Views and ViewSets
Generic views and viewsets reduce boilerplate code while remaining customizable. DRF provides ready-made classes for common patterns like list-create, retrieve-update-destroy, and pagination.
Authentication and Permissions
Built-in support for session authentication, token authentication, OAuth2, and JWT. Fine-grained permission classes control access at the view and object level.
Browsable API
DRF's browsable API provides an HTML interface for exploring and testing endpoints directly in the browser, improving developer experience significantly.
Content Negotiation
Automatic content negotiation supports multiple response formats (JSON, XML, YAML) based on client Accept headers.
Throttling
Rate limiting prevents API abuse with configurable throttle classes for anonymous and authenticated users.
Filtering and Search
Integration with django-filter and built-in search backends enable sophisticated query capabilities.
Project Setup
Let's build a comprehensive Todo API with authentication, validation, and proper error handling.
Initial Setup
# Create project directory
mkdir django_todo_api
cd django_todo_api
# Create and activate virtual environment
python3 -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install dependencies
pip install django djangorestframework django-filter python-decouple
# Create Django project
django-admin startproject config .
# Create todos app
python manage.py startapp todos
# Create requirements.txt
pip freeze > requirements.txt
Configuration
Configure settings.py:
# config/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Third-party apps
'rest_framework',
'django_filters',
# Local apps
'todos',
]
# REST Framework configuration
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10,
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
'rest_framework.filters.OrderingFilter',
],
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticatedOrReadOnly',
],
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
'rest_framework.renderers.BrowsableAPIRenderer',
],
'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler',
}
# Database configuration (using PostgreSQL in production)
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'todo_db',
'USER': 'postgres',
'PASSWORD': 'your_password',
'HOST': 'localhost',
'PORT': '5432',
}
}
# For development, you can use SQLite
# DATABASES = {
# 'default': {
# 'ENGINE': 'django.db.backends.sqlite3',
# 'NAME': BASE_DIR / 'db.sqlite3',
# }
# }
Building the Models
Create a comprehensive Todo model with all necessary fields:
# todos/models.py
from django.db import models
from django.contrib.auth.models import User
from django.core.validators import MinLengthValidator
class Todo(models.Model):
PRIORITY_CHOICES = [
('low', 'Low'),
('medium', 'Medium'),
('high', 'High'),
]
title = models.CharField(
max_length=200,
validators=[MinLengthValidator(3)],
help_text="Title of the todo item"
)
description = models.TextField(
blank=True,
help_text="Detailed description of the todo"
)
completed = models.BooleanField(
default=False,
help_text="Whether the todo is completed"
)
priority = models.CharField(
max_length=10,
choices=PRIORITY_CHOICES,
default='medium',
help_text="Priority level of the todo"
)
due_date = models.DateTimeField(
null=True,
blank=True,
help_text="Due date for the todo"
)
created_at = models.DateTimeField(
auto_now_add=True,
help_text="Timestamp when todo was created"
)
updated_at = models.DateTimeField(
auto_now=True,
help_text="Timestamp when todo was last updated"
)
owner = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='todos',
help_text="User who created this todo"
)
class Meta:
ordering = ['-created_at']
indexes = [
models.Index(fields=['owner', 'completed']),
models.Index(fields=['due_date']),
]
verbose_name = 'Todo'
verbose_name_plural = 'Todos'
def __str__(self):
return f"{self.title} - {'Completed' if self.completed else 'Pending'}"
def is_overdue(self):
"""Check if todo is past its due date"""
from django.utils import timezone
if self.due_date and not self.completed:
return timezone.now() > self.due_date
return False
Run Migrations
# Create migration files
python manage.py makemigrations
# Apply migrations
python manage.py migrate
# Create superuser for admin access
python manage.py createsuperuser
Creating Serializers
Serializers convert complex data types (like Django models) to Python datatypes that can be rendered into JSON, XML, or other content types.
# todos/serializers.py
from rest_framework import serializers
from django.contrib.auth.models import User
from .models import Todo
from django.utils import timezone
class UserSerializer(serializers.ModelSerializer):
"""Serializer for User model"""
todos_count = serializers.SerializerMethodField()
class Meta:
model = User
fields = ['id', 'username', 'email', 'todos_count']
read_only_fields = ['id']
def get_todos_count(self, obj):
return obj.todos.count()
class TodoSerializer(serializers.ModelSerializer):
"""Serializer for Todo model with full details"""
owner = UserSerializer(read_only=True)
is_overdue = serializers.SerializerMethodField()
days_until_due = serializers.SerializerMethodField()
class Meta:
model = Todo
fields = [
'id',
'title',
'description',
'completed',
'priority',
'due_date',
'created_at',
'updated_at',
'owner',
'is_overdue',
'days_until_due',
]
read_only_fields = ['id', 'created_at', 'updated_at', 'owner']
def get_is_overdue(self, obj):
"""Return whether the todo is overdue"""
return obj.is_overdue()
def get_days_until_due(self, obj):
"""Calculate days until due date"""
if obj.due_date and not obj.completed:
delta = obj.due_date - timezone.now()
return delta.days
return None
def validate_title(self, value):
"""Custom validation for title field"""
if len(value) < 3:
raise serializers.ValidationError(
"Title must be at least 3 characters long"
)
return value
def validate_due_date(self, value):
"""Ensure due date is not in the past"""
if value and value < timezone.now():
raise serializers.ValidationError(
"Due date cannot be in the past"
)
return value
def validate(self, data):
"""Object-level validation"""
if data.get('completed') and data.get('due_date'):
# Clear due date when marking as completed
data['due_date'] = None
return data
class TodoListSerializer(serializers.ModelSerializer):
"""Lightweight serializer for list views"""
owner_username = serializers.CharField(source='owner.username', read_only=True)
class Meta:
model = Todo
fields = [
'id',
'title',
'completed',
'priority',
'due_date',
'owner_username',
]
read_only_fields = ['id', 'owner_username']
class TodoCreateUpdateSerializer(serializers.ModelSerializer):
"""Serializer for creating and updating todos"""
class Meta:
model = Todo
fields = [
'title',
'description',
'completed',
'priority',
'due_date',
]
def create(self, validated_data):
"""Custom create method to set owner"""
validated_data['owner'] = self.context['request'].user
return super().create(validated_data)
Serializer Features Explained
SerializerMethodField
Adds custom computed fields to the serialized output. The method name must be get_<field_name>.
Custom Validation
Field-level validation with validate_<field_name> methods and object-level validation with the validate method catch errors before database operations.
Read-Only Fields
Fields marked as read-only are included in responses but ignored in create/update requests, preventing clients from modifying protected fields.
Source Attribute
The source parameter maps serializer fields to different model attributes or related fields.
Context
The serializer context provides access to the request object, view, and other contextual information.
Building Views
DRF provides several approaches to building views. We'll use ViewSets for clean, DRY code.
# todos/views.py
from rest_framework import viewsets, status, filters
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from django_filters.rest_framework import DjangoFilterBackend
from django.utils import timezone
from .models import Todo
from .serializers import (
TodoSerializer,
TodoListSerializer,
TodoCreateUpdateSerializer
)
from .permissions import IsOwnerOrReadOnly
from .filters import TodoFilter
class TodoViewSet(viewsets.ModelViewSet):
"""
ViewSet for Todo CRUD operations
Provides standard actions:
- list: GET /todos/
- create: POST /todos/
- retrieve: GET /todos/{id}/
- update: PUT /todos/{id}/
- partial_update: PATCH /todos/{id}/
- destroy: DELETE /todos/{id}/
Custom actions:
- complete: POST /todos/{id}/complete/
- statistics: GET /todos/statistics/
"""
queryset = Todo.objects.all()
permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
filter_backends = [
DjangoFilterBackend,
filters.SearchFilter,
filters.OrderingFilter
]
filterset_class = TodoFilter
search_fields = ['title', 'description']
ordering_fields = ['created_at', 'due_date', 'priority', 'completed']
ordering = ['-created_at']
def get_queryset(self):
"""Filter queryset to only show user's own todos"""
return Todo.objects.filter(owner=self.request.user).select_related('owner')
def get_serializer_class(self):
"""Return appropriate serializer based on action"""
if self.action == 'list':
return TodoListSerializer
elif self.action in ['create', 'update', 'partial_update']:
return TodoCreateUpdateSerializer
return TodoSerializer
def perform_create(self, serializer):
"""Set owner when creating a new todo"""
serializer.save(owner=self.request.user)
@action(detail=True, methods=['post'])
def complete(self, request, pk=None):
"""Mark a todo as completed"""
todo = self.get_object()
todo.completed = True
todo.save()
serializer = self.get_serializer(todo)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def uncomplete(self, request, pk=None):
"""Mark a todo as incomplete"""
todo = self.get_object()
todo.completed = False
todo.save()
serializer = self.get_serializer(todo)
return Response(serializer.data)
@action(detail=False, methods=['get'])
def statistics(self, request):
"""Get statistics about user's todos"""
queryset = self.get_queryset()
total = queryset.count()
completed = queryset.filter(completed=True).count()
pending = total - completed
overdue = sum(1 for todo in queryset if todo.is_overdue())
stats = {
'total': total,
'completed': completed,
'pending': pending,
'overdue': overdue,
'completion_rate': (completed / total * 100) if total > 0 else 0,
}
return Response(stats)
@action(detail=False, methods=['post'])
def bulk_complete(self, request):
"""Mark multiple todos as completed"""
todo_ids = request.data.get('ids', [])
if not todo_ids:
return Response(
{'error': 'No todo IDs provided'},
status=status.HTTP_400_BAD_REQUEST
)
queryset = self.get_queryset().filter(id__in=todo_ids)
updated = queryset.update(completed=True, updated_at=timezone.now())
return Response({
'message': f'Successfully completed {updated} todos',
'updated_count': updated
})
Custom Permissions
Create fine-grained permission control:
# todos/permissions.py
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
"""
Custom permission to only allow owners to edit their todos
"""
def has_object_permission(self, request, view, obj):
# Read permissions are allowed for any request
if request.method in permissions.SAFE_METHODS:
return True
# Write permissions only for the owner
return obj.owner == request.user
Custom Filters
Create sophisticated filtering capabilities:
# todos/filters.py
from django_filters import rest_framework as filters
from .models import Todo
class TodoFilter(filters.FilterSet):
"""Custom filters for Todo model"""
title = filters.CharFilter(lookup_expr='icontains')
created_after = filters.DateTimeFilter(field_name='created_at', lookup_expr='gte')
created_before = filters.DateTimeFilter(field_name='created_at', lookup_expr='lte')
due_soon = filters.BooleanFilter(method='filter_due_soon')
overdue = filters.BooleanFilter(method='filter_overdue')
class Meta:
model = Todo
fields = ['completed', 'priority', 'owner']
def filter_due_soon(self, queryset, name, value):
"""Filter todos due within the next 7 days"""
if value:
from django.utils import timezone
from datetime import timedelta
now = timezone.now()
soon = now + timedelta(days=7)
return queryset.filter(
due_date__gte=now,
due_date__lte=soon,
completed=False
)
return queryset
def filter_overdue(self, queryset, name, value):
"""Filter overdue todos"""
if value:
from django.utils import timezone
now = timezone.now()
return queryset.filter(
due_date__lt=now,
completed=False
)
return queryset
URL Configuration
Set up URL routing with the ViewSet router:
# config/urls.py
from django.contrib import admin
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from todos.views import TodoViewSet
# Create router and register viewsets
router = DefaultRouter()
router.register(r'todos', TodoViewSet, basename='todo')
urlpatterns = [
path('admin/', admin.site.urls),
path('api/v1/', include(router.urls)),
path('api-auth/', include('rest_framework.urls')), # Browsable API login
]
The DefaultRouter automatically creates these URLs:
GET /api/v1/todos/- List all todosPOST /api/v1/todos/- Create a new todoGET /api/v1/todos/{id}/- Retrieve a specific todoPUT /api/v1/todos/{id}/- Update a todo (full update)PATCH /api/v1/todos/{id}/- Partially update a todoDELETE /api/v1/todos/{id}/- Delete a todoPOST /api/v1/todos/{id}/complete/- Mark todo as completePOST /api/v1/todos/{id}/uncomplete/- Mark todo as incompleteGET /api/v1/todos/statistics/- Get user's todo statisticsPOST /api/v1/todos/bulk_complete/- Complete multiple todos
Admin Configuration
Customize the Django admin interface:
# todos/admin.py
from django.contrib import admin
from .models import Todo
@admin.register(Todo)
class TodoAdmin(admin.ModelAdmin):
list_display = [
'title',
'owner',
'priority',
'completed',
'due_date',
'created_at'
]
list_filter = ['completed', 'priority', 'created_at', 'due_date']
search_fields = ['title', 'description', 'owner__username']
readonly_fields = ['created_at', 'updated_at']
date_hierarchy = 'created_at'
fieldsets = (
('Basic Information', {
'fields': ('title', 'description', 'owner')
}),
('Status', {
'fields': ('completed', 'priority', 'due_date')
}),
('Timestamps', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
def get_queryset(self, request):
"""Optimize queries with select_related"""
queryset = super().get_queryset(request)
return queryset.select_related('owner')
Testing the API
Using cURL
List all todos:
curl -X GET http://localhost:8000/api/v1/todos/ \
-H "Authorization: Token your_token_here"
Create a new todo:
curl -X POST http://localhost:8000/api/v1/todos/ \
-H "Authorization: Token your_token_here" \
-H "Content-Type: application/json" \
-d '{
"title": "Complete Django tutorial",
"description": "Finish building the REST API",
"priority": "high",
"due_date": "2024-12-31T23:59:59Z"
}'
Update a todo:
curl -X PATCH http://localhost:8000/api/v1/todos/1/ \
-H "Authorization: Token your_token_here" \
-H "Content-Type: application/json" \
-d '{
"completed": true
}'
Filter todos:
curl -X GET "http://localhost:8000/api/v1/todos/?completed=false&priority=high&ordering=-due_date" \
-H "Authorization: Token your_token_here"
Get statistics:
curl -X GET http://localhost:8000/api/v1/todos/statistics/ \
-H "Authorization: Token your_token_here"
Complete a todo:
curl -X POST http://localhost:8000/api/v1/todos/1/complete/ \
-H "Authorization: Token your_token_here"
Delete a todo:
curl -X DELETE http://localhost:8000/api/v1/todos/1/ \
-H "Authorization: Token your_token_here"
Using Python Requests
import requests
BASE_URL = 'http://localhost:8000/api/v1'
TOKEN = 'your_token_here'
headers = {
'Authorization': f'Token {TOKEN}',
'Content-Type': 'application/json'
}
# List todos
response = requests.get(f'{BASE_URL}/todos/', headers=headers)
print(response.json())
# Create a todo
data = {
'title': 'Learn DRF',
'description': 'Master Django REST Framework',
'priority': 'high'
}
response = requests.post(f'{BASE_URL}/todos/', json=data, headers=headers)
print(response.json())
# Update a todo
data = {'completed': True}
response = requests.patch(f'{BASE_URL}/todos/1/', json=data, headers=headers)
print(response.json())
Automated Testing
Write comprehensive tests for your API:
# todos/tests.py
from django.test import TestCase
from django.contrib.auth.models import User
from rest_framework.test import APIClient
from rest_framework import status
from .models import Todo
class TodoAPITestCase(TestCase):
"""Test cases for Todo API endpoints"""
def setUp(self):
"""Set up test client and create test user"""
self.client = APIClient()
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.client.force_authenticate(user=self.user)
# Create test todo
self.todo = Todo.objects.create(
title='Test Todo',
description='Test Description',
owner=self.user
)
def test_list_todos(self):
"""Test retrieving list of todos"""
response = self.client.get('/api/v1/todos/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data['results']), 1)
def test_create_todo(self):
"""Test creating a new todo"""
data = {
'title': 'New Todo',
'description': 'New Description',
'priority': 'high'
}
response = self.client.post('/api/v1/todos/', data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Todo.objects.count(), 2)
self.assertEqual(response.data['title'], 'New Todo')
def test_create_todo_invalid_data(self):
"""Test creating todo with invalid data"""
data = {'title': 'AB'} # Too short
response = self.client.post('/api/v1/todos/', data)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test_retrieve_todo(self):
"""Test retrieving a specific todo"""
response = self.client.get(f'/api/v1/todos/{self.todo.id}/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['title'], 'Test Todo')
def test_update_todo(self):
"""Test updating a todo"""
data = {'completed': True}
response = self.client.patch(f'/api/v1/todos/{self.todo.id}/', data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.todo.refresh_from_db()
self.assertTrue(self.todo.completed)
def test_delete_todo(self):
"""Test deleting a todo"""
response = self.client.delete(f'/api/v1/todos/{self.todo.id}/')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertEqual(Todo.objects.count(), 0)
def test_complete_action(self):
"""Test custom complete action"""
response = self.client.post(f'/api/v1/todos/{self.todo.id}/complete/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.todo.refresh_from_db()
self.assertTrue(self.todo.completed)
def test_statistics(self):
"""Test statistics endpoint"""
response = self.client.get('/api/v1/todos/statistics/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('total', response.data)
self.assertIn('completed', response.data)
def test_unauthorized_access(self):
"""Test that unauthenticated users can't access todos"""
self.client.force_authenticate(user=None)
response = self.client.get('/api/v1/todos/')
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test_owner_isolation(self):
"""Test that users can only see their own todos"""
other_user = User.objects.create_user(
username='otheruser',
password='otherpass123'
)
Todo.objects.create(
title='Other Todo',
description='Other Description',
owner=other_user
)
response = self.client.get('/api/v1/todos/')
self.assertEqual(len(response.data['results']), 1)
self.assertEqual(response.data['results'][0]['title'], 'Test Todo')
# Run tests with:
# python manage.py test
Authentication Implementation
Token Authentication
Enable token authentication:
# Add to INSTALLED_APPS
INSTALLED_APPS = [
# ...
'rest_framework.authtoken',
]
# Run migrations
python manage.py migrate
Create authentication views:
# todos/views.py (add these)
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.authtoken.models import Token
from rest_framework.response import Response
class CustomAuthToken(ObtainAuthToken):
"""Custom token authentication endpoint"""
def post(self, request, *args, **kwargs):
serializer = self.serializer_class(
data=request.data,
context={'request': request}
)
serializer.is_valid(raise_exception=True)
user = serializer.validated_data['user']
token, created = Token.objects.get_or_create(user=user)
return Response({
'token': token.key,
'user_id': user.pk,
'username': user.username,
'email': user.email
})
Add to URLs:
# config/urls.py
from todos.views import CustomAuthToken
urlpatterns = [
# ...
path('api/v1/auth/login/', CustomAuthToken.as_view(), name='api_token_auth'),
]
JWT Authentication (Alternative)
For JWT authentication, install djangorestframework-simplejwt:
pip install djangorestframework-simplejwt
Configure settings:
# config/settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
}
from datetime import timedelta
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
'ROTATE_REFRESH_TOKENS': False,
'BLACKLIST_AFTER_ROTATION': False,
}
Add JWT URLs:
# config/urls.py
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
urlpatterns = [
# ...
path('api/v1/auth/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/v1/auth/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]
Advanced Features
Pagination
Custom pagination class:
# todos/pagination.py
from rest_framework.pagination import PageNumberPagination
from rest_framework.response import Response
class CustomPagination(PageNumberPagination):
page_size = 10
page_size_query_param = 'page_size'
max_page_size = 100
def get_paginated_response(self, data):
return Response({
'links': {
'next': self.get_next_link(),
'previous': self.get_previous_link()
},
'count': self.page.paginator.count,
'total_pages': self.page.paginator.num_pages,
'results': data
})
Throttling
Configure rate limiting:
# config/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 throttle class:
# todos/throttling.py
from rest_framework.throttling import UserRateThrottle
class BurstRateThrottle(UserRateThrottle):
scope = 'burst'
rate = '60/min'
class SustainedRateThrottle(UserRateThrottle):
scope = 'sustained'
rate = '1000/day'
Versioning
Implement API versioning:
# config/settings.py
REST_FRAMEWORK = {
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
'DEFAULT_VERSION': 'v1',
'ALLOWED_VERSIONS': ['v1', 'v2'],
}
# config/urls.py
urlpatterns = [
path('api/<str:version>/', include(router.urls)),
]
CORS Configuration
For frontend applications on different domains:
pip install django-cors-headers
# config/settings.py
INSTALLED_APPS = [
# ...
'corsheaders',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
# ...
]
# Development settings
CORS_ALLOW_ALL_ORIGINS = True
# Production settings
CORS_ALLOWED_ORIGINS = [
"https://yourdomain.com",
"https://www.yourdomain.com",
]
Deployment Considerations
Environment Variables
Use python-decouple for configuration:
# config/settings.py
from decouple import config
SECRET_KEY = config('SECRET_KEY')
DEBUG = config('DEBUG', default=False, cast=bool)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', cast=lambda v: [s.strip() for s in v.split(',')])
Create .env file:
SECRET_KEY=your-secret-key-here
DEBUG=False
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
DATABASE_URL=postgresql://user:password@localhost/dbname
Production Settings
# config/settings.py
if not DEBUG:
# Security settings
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
# HTTPS settings
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# HSTS settings
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
Static Files
# config/settings.py
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
Collect static files:
python manage.py collectstatic
Database Configuration
For PostgreSQL in production:
import dj_database_url
DATABASES = {
'default': dj_database_url.config(
default=config('DATABASE_URL'),
conn_max_age=600,
conn_health_checks=True,
)
}
API Documentation
Using drf-spectacular
Install OpenAPI documentation generator:
pip install drf-spectacular
Configure:
# config/settings.py
INSTALLED_APPS = [
# ...
'drf_spectacular',
]
REST_FRAMEWORK = {
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}
SPECTACULAR_SETTINGS = {
'TITLE': 'Todo API',
'DESCRIPTION': 'A comprehensive Todo management API',
'VERSION': '1.0.0',
'SERVE_INCLUDE_SCHEMA': False,
}
Add URLs:
# config/urls.py
from drf_spectacular.views import (
SpectacularAPIView,
SpectacularRedocView,
SpectacularSwaggerView
)
urlpatterns = [
# ...
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
]
Access documentation at:
- Swagger UI: http://localhost:8000/api/docs/
- ReDoc: http://localhost:8000/api/redoc/
Performance Optimization
Database Query Optimization
# Optimize queries with select_related and prefetch_related
queryset = Todo.objects.select_related('owner').prefetch_related('tags')
# Use only() to fetch specific fields
queryset = Todo.objects.only('title', 'completed')
# Use defer() to exclude fields
queryset = Todo.objects.defer('description')
Caching
Configure Redis caching:
pip install django-redis
# config/settings.py
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
}
}
}
Use caching in views:
from django.views.decorators.cache import cache_page
from django.utils.decorators import method_decorator
class TodoViewSet(viewsets.ModelViewSet):
@method_decorator(cache_page(60 * 15)) # Cache for 15 minutes
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
Summary
In this comprehensive guide, we've covered:
Core Concepts
- REST API principles and best practices
- Django and DRF architecture
- HTTP methods and status codes
Implementation
- Project setup and configuration
- Model design with relationships and validation
- Serializers with custom validation and computed fields
- ViewSets with custom actions
- URL routing with routers
- Custom permissions and filters
Advanced Features
- Token and JWT authentication
- Pagination and throttling
- API versioning
- CORS configuration
- Automated testing
Production Readiness
- Environment configuration
- Security settings
- Database optimization
- Caching strategies
- API documentation with OpenAPI/Swagger
Django REST Framework provides a robust, flexible foundation for building production-ready APIs. Its comprehensive feature set, excellent documentation, and active community make it an ideal choice for API development in Python.
The framework's design promotes best practices, handles common tasks automatically, and remains flexible for custom requirements. Whether building a simple CRUD API or a complex microservices architecture, DRF provides the tools needed to succeed.
Additional Resources
Official Documentation
Authentication
Testing
Deployment
Best Practices
Happy coding!