About us Guides Projects Contacts
Админка
please wait

Django REST Framework (DRF) is the premier toolkit for building web APIs with Django. It provides serialization, authentication, viewsets, and browsable API documentation out of the box. This guide covers building production-ready REST APIs from a senior developer's perspective.

Why Django REST Framework

DRF offers compelling advantages:

  1. Rapid Development: Viewsets and routers minimize boilerplate
  2. Serialization: Complex data types to native Python and back
  3. Authentication: Token, session, JWT, and OAuth support
  4. Browsable API: Interactive documentation for free
  5. Throttling: Built-in rate limiting

Setup and Installation

Create Django Project

# Create a virtual environment
python -m venv venv
source venv/bin/activate # Linux/macOS
# venv\Scripts\activate # Windows
# Install Django and DRF
pip install django djangorestframework
pip install django-filter # For filtering
# Create the project and app
django-admin startproject myproject
cd myproject
python manage.py startapp api

Configure Settings

myproject/settings.py:

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Third-party
'rest_framework',
'django_filters',
# Local
'api',
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 20,
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
'rest_framework.filters.OrderingFilter',
],
}

Models

api/models.py:

from django.db import models
from django.contrib.auth.models import User
class Category(models.Model):
name = models.CharField(max_length=100)
slug = models.SlugField(unique=True)
class Meta:
verbose_name_plural = 'categories'
def __str__(self):
return self.name
class Post(models.Model):
STATUS_CHOICES = [
('draft', 'Draft'),
('published', 'Published'),
]
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
content = models.TextField()
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, related_name='posts')
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
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)
class Meta:
ordering = ['created_at']

Run migrations:

python manage.py makemigrations
python manage.py migrate

Serializers

api/serializers.py:

from rest_framework import serializers
from django.contrib.auth.models import User
from .models import Post, Category, Comment
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email', 'first_name', 'last_name']
read_only_fields = ['id']
class CategorySerializer(serializers.ModelSerializer):
post_count = serializers.SerializerMethodField()
class Meta:
model = Category
fields = ['id', 'name', 'slug', 'post_count']
def get_post_count(self, obj):
return obj.posts.count()
class CommentSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True)
class Meta:
model = Comment
fields = ['id', 'author', 'content', 'created_at']
read_only_fields = ['id', 'created_at']
class PostListSerializer(serializers.ModelSerializer):
"""Lightweight serializer for list views"""
author = UserSerializer(read_only=True)
category = CategorySerializer(read_only=True)
comment_count = serializers.SerializerMethodField()
class Meta:
model = Post
fields = ['id', 'title', 'slug', 'author', 'category',
'status', 'created_at', 'comment_count']
def get_comment_count(self, obj):
return obj.comments.count()
class PostDetailSerializer(serializers.ModelSerializer):
"""Full serializer with nested comments"""
author = UserSerializer(read_only=True)
category = CategorySerializer(read_only=True)
category_id = serializers.PrimaryKeyRelatedField(
queryset=Category.objects.all(),
source='category',
write_only=True,
required=False
)
comments = CommentSerializer(many=True, read_only=True)
class Meta:
model = Post
fields = ['id', 'title', 'slug', 'content', 'author',
'category', 'category_id', 'status',
'created_at', 'updated_at', 'comments']
read_only_fields = ['id', 'created_at', 'updated_at']
def validate_title(self, value):
if len(value) < 5:
raise serializers.ValidationError("Title must be at least 5 characters")
return value

ViewSets and Views

api/views.py:

from rest_framework import viewsets, permissions, status, filters
from rest_framework.decorators import action
from rest_framework.response import Response
from django_filters.rest_framework import DjangoFilterBackend
from .models import Post, Category, Comment
from .serializers import (
PostListSerializer, PostDetailSerializer,
CategorySerializer, CommentSerializer
)
class IsAuthorOrReadOnly(permissions.BasePermission):
"""Custom permission: only author can edit/delete"""
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.author == request.user
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.select_related('author', 'category').prefetch_related('comments')
permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsAuthorOrReadOnly]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['status', 'category', 'author']
search_fields = ['title', 'content']
ordering_fields = ['created_at', 'title']
lookup_field = 'slug'
def get_serializer_class(self):
if self.action == 'list':
return PostListSerializer
return PostDetailSerializer
def get_queryset(self):
queryset = super().get_queryset()
# Non-authenticated users see only published posts
if not self.request.user.is_authenticated:
queryset = queryset.filter(status='published')
# Authenticated users see their own posts + published
elif not self.request.user.is_staff:
queryset = queryset.filter(
models.Q(status='published') | models.Q(author=self.request.user)
)
return queryset
def perform_create(self, serializer):
serializer.save(author=self.request.user)
@action(detail=True, methods=['post'])
def publish(self, request, slug=None):
post = self.get_object()
if post.author != request.user and not request.user.is_staff:
return Response(
{'error': 'Only author can publish'},
status=status.HTTP_403_FORBIDDEN
)
post.status = 'published'
post.save()
return Response({'status': 'published'})
@action(detail=True, methods=['get', 'post'])
def comments(self, request, slug=None):
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(author=request.user, post=post)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class CategoryViewSet(viewsets.ModelViewSet):
queryset = Category.objects.all()
serializer_class = CategorySerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
lookup_field = 'slug'

URL Routing

api/urls.py:

from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views
router = DefaultRouter()
router.register(r'posts', views.PostViewSet)
router.register(r'categories', views.CategoryViewSet)
urlpatterns = [
path('', include(router.urls)),
]

myproject/urls.py:

from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('api.urls')),
path('api-auth/', include('rest_framework.urls')), # Browsable API login
]

Authentication

Token Authentication

pip install djangorestframework-simplejwt

myproject/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=7),
}

api/urls.py:

from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
urlpatterns = [
# ... existing routes
path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]

Usage:

# Get tokens
curl -X POST http://localhost:8000/api/token/ \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"password"}'
# Use the access token
curl http://localhost:8000/api/posts/ \
-H "Authorization: Bearer <access_token>"

Filtering and Search

api/filters.py:

import django_filters
from .models import Post
class PostFilter(django_filters.FilterSet):
title = django_filters.CharFilter(lookup_expr='icontains')
created_after = django_filters.DateTimeFilter(
field_name='created_at',
lookup_expr='gte'
)
created_before = django_filters.DateTimeFilter(
field_name='created_at',
lookup_expr='lte'
)
class Meta:
model = Post
fields = ['status', 'category', 'author', 'title',
'created_after', 'created_before']

Use in viewset:

from .filters import PostFilter
class PostViewSet(viewsets.ModelViewSet):
filterset_class = PostFilter
# ...

Testing

api/tests.py:

from django.test import TestCase
from django.contrib.auth.models import User
from rest_framework.test import APITestCase, APIClient
from rest_framework import status
from .models import Post, Category
class PostAPITests(APITestCase):
def setUp(self):
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.category = Category.objects.create(
name='Tech',
slug='tech'
)
self.post = Post.objects.create(
title='Test Post',
slug='test-post',
content='Test content',
author=self.user,
category=self.category,
status='published'
)
self.client = APIClient()
def test_list_posts(self):
response = self.client.get('/api/posts/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data['results']), 1)
def test_create_post_authenticated(self):
self.client.force_authenticate(user=self.user)
data = {
'title': 'New Post',
'slug': 'new-post',
'content': 'New content',
'category_id': self.category.id,
}
response = self.client.post('/api/posts/', data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Post.objects.count(), 2)
def test_create_post_unauthenticated(self):
data = {'title': 'New Post', 'content': 'Content'}
response = self.client.post('/api/posts/', data)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test_update_own_post(self):
self.client.force_authenticate(user=self.user)
response = self.client.patch(
f'/api/posts/{self.post.slug}/',
{'title': 'Updated Title'}
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.post.refresh_from_db()
self.assertEqual(self.post.title, 'Updated Title')
def test_cannot_update_others_post(self):
other_user = User.objects.create_user('other', password='pass')
self.client.force_authenticate(user=other_user)
response = self.client.patch(
f'/api/posts/{self.post.slug}/',
{'title': 'Hacked'}
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

Run tests:

python manage.py test api

Key Takeaways

  1. Use ViewSets: Minimize repetitive code for CRUD operations
  2. Custom permissions: Implement business logic in permission classes
  3. Optimize queries: Use selectrelated and prefetchrelated
  4. Different serializers: List vs. detail views need different data
  5. Filter backends: Enable powerful querying without custom code
  6. Test thoroughly: API tests catch integration issues early

Django REST Framework provides everything needed for robust, secure APIs while maintaining the productivity Django is known for.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
ZK Interactive