Django REST Framework Tutorial: Building REST APIs with Django
Introduction
Django REST Framework (DRF) is a powerful and flexible toolkit for building Web APIs in Django. It provides a set of tools and utilities that make it easy to build RESTful APIs with Django, including serializers, viewsets, authentication, permissions, pagination, and automatic API documentation.
This comprehensive tutorial will walk you through building a complete REST API using Django REST Framework. You'll learn how to create serializers, viewsets, handle authentication and permissions, implement filtering and pagination, and generate automatic API documentation. By the end of this tutorial, you'll have a solid understanding of building production-ready REST APIs with Django.
Prerequisites
Before starting, make sure you have:
- Django knowledge: Basic understanding of Django (models, views, URLs)
- Python 3.8+: Installed on your system
- Django project: An existing Django project (or follow our Django Beginner Tutorial)
- Basic API knowledge: Understanding of REST principles (GET, POST, PUT, DELETE)
- Code editor: VS Code, PyCharm, or any editor you prefer
- Terminal/Command Prompt: Access to run commands If you haven't created a Django project yet, check out our Django Beginner Tutorial first.
Step 1: Installing Django REST Framework
First, let's install Django REST Framework in your Django project:
# Activate your virtual environment first
# On Windows: venv\Scripts\Activate.ps1
# On macOS/Linux: source venv/bin/activate
pip install djangorestframework
Open your `settings.py` file and add `rest_framework` to `INSTALLED_APPS`:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework', # Add Django REST Framework
'blog', # Your app
]
Add REST framework settings to `settings.py`:
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10,
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticatedOrReadOnly',
],
}
python manage.py shell
>>> import rest_framework
>>> rest_framework.VERSION
You should see the DRF version number.
Step 2: Creating Models
Let's create models for our API. We'll use a blog example with Post and Comment models:
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
class Post(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(max_length=200, unique=True)
content = models.TextField()
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
published_at = models.DateTimeField(null=True, blank=True)
status = models.CharField(
max_length=10,
choices=[('draft', 'Draft'), ('published', 'Published')],
default='draft'
)
class Meta:
ordering = ['-created_at']
def __str__(self):
return self.title
class Comment(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
author = models.ForeignKey(User, on_delete=models.CASCADE)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created_at']
def __str__(self):
return f'Comment by {self.author.username} on {self.post.title}'
python manage.py makemigrations
python manage.py migrate
Step 3: Creating Serializers
Serializers convert model instances to JSON and vice versa. Create `blog/serializers.py`:
from rest_framework import serializers
from django.contrib.auth.models import User
from .models import Post, Comment
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email', 'first_name', 'last_name']
class CommentSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True)
class Meta:
model = Comment
fields = ['id', 'content', 'author', 'created_at', 'updated_at']
read_only_fields = ['author', 'created_at', 'updated_at']
class PostSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True)
comments = CommentSerializer(many=True, read_only=True)
author_username = serializers.CharField(source='author.username', read_only=True)
class Meta:
model = Post
fields = [
'id', 'title', 'slug', 'content', 'author', 'author_username',
'created_at', 'updated_at', 'published_at', 'status', 'comments'
]
read_only_fields = ['author', 'created_at', 'updated_at']
def create(self, validated_data):
# Set author to current user
validated_data['author'] = self.context['request'].user
return super().create(validated_data)
class PostListSerializer(serializers.ModelSerializer):
"""Simplified serializer for list view"""
author_username = serializers.CharField(source='author.username', read_only=True)
comment_count = serializers.IntegerField(source='comments.count', read_only=True)
class Meta:
model = Post
fields = [
'id', 'title', 'slug', 'author_username',
'created_at', 'published_at', 'status', 'comment_count'
]
Step 4: Creating Viewsets
Viewsets combine the logic for multiple related views. Create `blog/views.py`:
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticatedOrReadOnly, IsAuthenticated
from django.shortcuts import get_object_or_404
from .models import Post, Comment
from .serializers import PostSerializer, PostListSerializer, CommentSerializer
class PostViewSet(viewsets.ModelViewSet):
"""
ViewSet for viewing and editing Post instances.
Provides list, create, retrieve, update, and destroy actions.
"""
queryset = Post.objects.all()
permission_classes = [IsAuthenticatedOrReadOnly]
def get_serializer_class(self):
if self.action == 'list':
return PostListSerializer
return PostSerializer
def get_queryset(self):
queryset = Post.objects.all()
status_param = self.request.query_params.get('status', None)
author_param = self.request.query_params.get('author', None)
if status_param:
queryset = queryset.filter(status=status_param)
if author_param:
queryset = queryset.filter(author__username=author_param)
return queryset
@action(detail=True, methods=['post'], permission_classes=[IsAuthenticated])
def publish(self, request, pk=None):
"""Custom action to publish a post"""
post = self.get_object()
if post.author != request.user:
return Response(
{'error': 'You can only publish your own posts.'},
status=status.HTTP_403_FORBIDDEN
)
post.status = 'published'
post.published_at = timezone.now()
post.save()
return Response({'status': 'post published'})
@action(detail=True, methods=['get', 'post'])
def comments(self, request, pk=None):
"""Custom action to get or create comments for a post"""
post = self.get_object()
if request.method == 'GET':
comments = post.comments.all()
serializer = CommentSerializer(comments, many=True)
return Response(serializer.data)
elif request.method == 'POST':
serializer = CommentSerializer(data=request.data)
if serializer.is_valid():
serializer.save(post=post, author=request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class CommentViewSet(viewsets.ModelViewSet):
"""
ViewSet for viewing and editing Comment instances.
"""
queryset = Comment.objects.all()
serializer_class = CommentSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
def perform_create(self, serializer):
serializer.save(author=self.request.user)
Step 5: Setting Up URLs with Routers
DRF routers automatically generate URL patterns for viewsets. Create `blog/urls.py`:
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import PostViewSet, CommentViewSet
# Create router and register viewsets
router = DefaultRouter()
router.register(r'posts', PostViewSet, basename='post')
router.register(r'comments', CommentViewSet, basename='comment')
app_name = 'blog'
urlpatterns = [
path('', include(router.urls)),
]
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('blog.urls')), # API endpoints
path('api-auth/', include('rest_framework.urls')), # Login/logout for browsable API
]
Available Endpoints:
- `GET /api/posts/` - List all posts
- `POST /api/posts/` - Create a new post
- `GET /api/posts/{id}/` - Retrieve a post
- `PUT /api/posts/{id}/` - Update a post
- `PATCH /api/posts/{id}/` - Partially update a post
- `DELETE /api/posts/{id}/` - Delete a post
- `POST /api/posts/{id}/publish/` - Custom action: publish post
- `GET /api/posts/{id}/comments/` - Get comments for a post
- `POST /api/posts/{id}/comments/` - Create comment for a post
Step 6: Authentication and Permissions
DRF provides multiple authentication methods. Let's set up token authentication:
pip install djangorestframework-simplejwt
INSTALLED_APPS = [
# ... other apps
'rest_framework',
'rest_framework_simplejwt', # JWT authentication
]
from datetime import timedelta
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
'rest_framework.authentication.SessionAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticatedOrReadOnly',
],
}
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
}
from django.urls import path, include
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('blog.urls')),
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]
from rest_framework import permissions
class IsAuthorOrReadOnly(permissions.BasePermission):
"""
Custom permission to only allow authors to edit their own posts.
"""
def has_object_permission(self, request, view, obj):
# Read permissions for any request
if request.method in permissions.SAFE_METHODS:
return True
# Write permissions only to the author
return obj.author == request.user
from .permissions import IsAuthorOrReadOnly
class PostViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticatedOrReadOnly, IsAuthorOrReadOnly]
# ... rest of the code
Step 7: Filtering and Search
Add filtering and search capabilities using django-filter:
pip install django-filter
INSTALLED_APPS = [
# ... other apps
'django_filters',
]
REST_FRAMEWORK = {
# ... other settings
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
'rest_framework.filters.OrderingFilter',
],
}
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['status', 'author']
search_fields = ['title', 'content']
ordering_fields = ['created_at', 'published_at', 'title']
ordering = ['-created_at']
Usage Examples:
- `GET /api/posts/?status=published` - Filter by status
- `GET /api/posts/?author=username` - Filter by author
- `GET /api/posts/?search=django` - Search in title and content
- `GET /api/posts/?ordering=-created_at` - Order by creation date
- `GET /api/posts/?status=published&search=django&ordering=-published_at` - Combined filters
Step 8: Pagination
DRF provides built-in pagination classes. Configure pagination in `settings.py`:
REST_FRAMEWORK = {
# ... other settings
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10,
}
from rest_framework.pagination import PageNumberPagination
class PostPagination(PageNumberPagination):
page_size = 10
page_size_query_param = 'page_size'
max_page_size = 100
from .pagination import PostPagination
class PostViewSet(viewsets.ModelViewSet):
pagination_class = PostPagination
# ... rest of the code
{
"count": 50,
"next": "http://example.com/api/posts/?page=3",
"previous": "http://example.com/api/posts/?page=1",
"results": [
// ... post objects
]
}
Step 9: API Documentation with Swagger/OpenAPI
Generate automatic API documentation using drf-yasg or drf-spectacular:
pip install drf-spectacular
INSTALLED_APPS = [
# ... other apps
'drf_spectacular',
]
REST_FRAMEWORK = {
# ... other settings
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}
SPECTACULAR_SETTINGS = {
'TITLE': 'Blog API',
'DESCRIPTION': 'A comprehensive blog API built with Django REST Framework',
'VERSION': '1.0.0',
'SERVE_INCLUDE_SCHEMA': False,
}
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('blog.urls')),
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
]
Access Documentation:
- Swagger UI: `http://127.0.0.1:8000/api/docs/`
- ReDoc: `http://127.0.0.1:8000/api/schema/redoc/`
- OpenAPI Schema: `http://127.0.0.1:8000/api/schema/`
Step 10: Testing the API
DRF provides a test client for testing APIs. Create `blog/tests.py`:
from django.contrib.auth.models import User
from django.urls import reverse
from rest_framework.test import APITestCase, APIClient
from rest_framework import status
from .models import Post
class PostAPITestCase(APITestCase):
def setUp(self):
self.client = APIClient()
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.post = Post.objects.create(
title='Test Post',
slug='test-post',
content='Test content',
author=self.user,
status='published'
)
def test_list_posts(self):
"""Test listing all posts"""
url = reverse('post-list')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data['results']), 1)
def test_create_post(self):
"""Test creating a new post"""
self.client.force_authenticate(user=self.user)
url = reverse('post-list')
data = {
'title': 'New Post',
'slug': 'new-post',
'content': 'New content'
}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Post.objects.count(), 2)
def test_retrieve_post(self):
"""Test retrieving a single post"""
url = reverse('post-detail', kwargs={'pk': self.post.pk})
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['title'], 'Test Post')
def test_update_post(self):
"""Test updating a post"""
self.client.force_authenticate(user=self.user)
url = reverse('post-detail', kwargs={'pk': self.post.pk})
data = {'title': 'Updated Post'}
response = self.client.patch(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.post.refresh_from_db()
self.assertEqual(self.post.title, 'Updated Post')
def test_delete_post(self):
"""Test deleting a post"""
self.client.force_authenticate(user=self.user)
url = reverse('post-detail', kwargs={'pk': self.post.pk})
response = self.client.delete(url)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertEqual(Post.objects.count(), 0)
def test_publish_action(self):
"""Test custom publish action"""
self.client.force_authenticate(user=self.user)
url = reverse('post-publish', kwargs={'pk': self.post.pk})
response = self.client.post(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.post.refresh_from_db()
self.assertEqual(self.post.status, 'published')
python manage.py test blog.tests
Step 11: API Versioning
Implement API versioning for backward compatibility:
REST_FRAMEWORK = {
# ... other settings
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
'DEFAULT_VERSION': 'v1',
'ALLOWED_VERSIONS': ['v1', 'v2'],
'VERSION_PARAM': 'version',
}
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/v1/', include('blog.urls')), # Version 1
path('api/v2/', include('blog.v2.urls')), # Version 2
]
Access Versioned API:
- `GET /api/v1/posts/` - Version 1
- `GET /api/v2/posts/` - Version 2
Step 12: Throttling (Rate Limiting)
Implement rate limiting to prevent API abuse:
REST_FRAMEWORK = {
# ... other settings
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle'
],
'DEFAULT_THROTTLE_RATES': {
'anon': '100/hour',
'user': '1000/hour'
}
}
from rest_framework.throttling import UserRateThrottle
class PostCreateThrottle(UserRateThrottle):
scope = 'post_create'
'DEFAULT_THROTTLE_RATES': {
'anon': '100/hour',
'user': '1000/hour',
'post_create': '10/hour', # Custom throttle
}
from .throttling import PostCreateThrottle
class PostViewSet(viewsets.ModelViewSet):
throttle_classes = [PostCreateThrottle]
# ... rest of the code
Step 13: Error Handling
Customize error handling for better API responses:
Create `blog/exceptions.py`:
from rest_framework.views import exception_handler
from rest_framework.response import Response
from rest_framework import status
def custom_exception_handler(exc, context):
# Call REST framework's default exception handler first
response = exception_handler(exc, context)
# Customize the response
if response is not None:
custom_response_data = {
'error': {
'status_code': response.status_code,
'message': 'An error occurred',
'details': response.data
}
}
response.data = custom_response_data
return response
REST_FRAMEWORK = {
# ... other settings
'EXCEPTION_HANDLER': 'blog.exceptions.custom_exception_handler',
}
Step 14: Testing API with HTTPie or cURL
Test your API using HTTPie or cURL:
pip install httpie
# Get access token
http POST http://127.0.0.1:8000/api/token/ username=testuser password=testpass123
# List posts
http GET http://127.0.0.1:8000/api/posts/
# Create post (with authentication)
http POST http://127.0.0.1:8000/api/posts/ \
"Authorization: Bearer YOUR_ACCESS_TOKEN" \
title="New Post" \
slug="new-post" \
content="Post content"
# Update post
http PATCH http://127.0.0.1:8000/api/posts/1/ \
"Authorization: Bearer YOUR_ACCESS_TOKEN" \
title="Updated Post"
# Delete post
http DELETE http://127.0.0.1:8000/api/posts/1/ \
"Authorization: Bearer YOUR_ACCESS_TOKEN"
# Custom action
http POST http://127.0.0.1:8000/api/posts/1/publish/ \
"Authorization: Bearer YOUR_ACCESS_TOKEN"
# Get access token
curl -X POST http://127.0.0.1:8000/api/token/ \
-H "Content-Type: application/json" \
-d '{"username":"testuser","password":"testpass123"}'
# List posts
curl http://127.0.0.1:8000/api/posts/
# Create post
curl -X POST http://127.0.0.1:8000/api/posts/ \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"title":"New Post","slug":"new-post","content":"Post content"}'
Common Django REST Framework Commands
Quick reference for common DRF commands:
# Install DRF
pip install djangorestframework
# Install JWT authentication
pip install djangorestframework-simplejwt
# Install filtering
pip install django-filter
# Install API documentation
pip install drf-spectacular
# Run development server
python manage.py runserver
# Run tests
python manage.py test
# Create migrations
python manage.py makemigrations
# Apply migrations
python manage.py migrate
# Test with HTTPie
http GET http://127.0.0.1:8000/api/posts/
# Test with cURL
curl http://127.0.0.1:8000/api/posts/
Best Practices
Follow these best practices when building REST APIs with DRF:
1. Use ViewSets for CRUD Operations:
- ViewSets reduce code duplication
- Automatic URL routing
- Consistent API structure
2. Separate Serializers for Different Actions:
- Use different serializers for list vs detail views
- Optimize data transfer
- Improve performance
3. Implement Proper Authentication:
- Use JWT for stateless authentication
- Implement token refresh
- Secure token storage
4. Set Appropriate Permissions:
- Use custom permissions when needed
- Follow principle of least privilege
- Document permission requirements
5. Implement Filtering and Search:
- Use django-filter for complex filtering
- Implement search functionality
- Add ordering capabilities
6. Use Pagination:
- Always paginate large datasets
- Allow configurable page size
- Provide pagination metadata
7. Version Your API:
- Plan for API versioning from the start
- Maintain backward compatibility
- Document version changes
8. Handle Errors Gracefully:
- Custom exception handlers
- Meaningful error messages
- Proper HTTP status codes
9. Document Your API:
- Use automatic documentation tools
- Document all endpoints
- Provide examples
10. Test Thoroughly:
- Write unit tests
- Test authentication and permissions
- Test edge cases
Next Steps and Learning Resources
Congratulations! You've built a complete REST API with Django REST Framework. Here's what to learn next:
Advanced Topics:
- Nested Serializers: Complex nested relationships
- Custom ViewSets: Custom actions and logic
- API Permissions: Advanced permission classes
- Caching: Redis caching for API responses
- WebSockets: Real-time features with Django Channels
- GraphQL: GraphQL APIs with Django
- API Gateway: API gateway patterns
- Microservices: Building microservices with DRF
Best Practices:
- API Design: RESTful API design principles
- Security: API security best practices
- Performance: API optimization techniques
- Testing: Comprehensive API testing
- Documentation: API documentation standards
Learning Resources:
- Official DRF Documentation: https://www.django-rest-framework.org/
- DRF Tutorial: Official tutorial on DRF website
- Django REST Framework: Book by William S. Vincent
- Real Python: DRF tutorials and articles
- TestDriven.io: DRF tutorials and courses
Community:
- DRF GitHub: https://github.com/encode/django-rest-framework
- Stack Overflow: Q&A platform
- Django Forum: Community discussions
- Reddit r/django: Community discussions
Conclusion
You've successfully built a complete REST API with Django REST Framework! You've learned:
- ✅ Installing and configuring Django REST Framework
- ✅ Creating serializers for data conversion
- ✅ Building viewsets for CRUD operations
- ✅ Setting up URL routing with routers
- ✅ Implementing authentication and permissions
- ✅ Adding filtering, search, and pagination
- ✅ Generating API documentation
- ✅ Testing APIs
- ✅ API versioning and throttling
- ✅ Error handling
Key Takeaways:
- DRF makes building REST APIs easy and efficient
- Serializers handle data conversion automatically
- ViewSets provide powerful CRUD operations
- Routers generate URLs automatically
- Authentication and permissions are built-in
- Filtering and pagination improve API usability
- Automatic documentation saves time
- Testing is essential for API reliability
Remember:
- Follow RESTful principles
- Implement proper authentication
- Use appropriate permissions
- Document your API
- Test thoroughly
- Version your API
- Handle errors gracefully
- Optimize for performance Django REST Framework is a powerful tool for building production-ready REST APIs. Continue practicing, building projects, and exploring advanced features. Happy coding!