О нас Руководства Проекты Контакты
Админка
пожалуйста подождите

Django REST Framework (DRF) — ведущий инструментарий для создания Web API на Django. Он предоставляет сериализацию, аутентификацию, viewsets и интерактивную документацию API «из коробки». Это руководство посвящено созданию готовых к production REST API с точки зрения senior-разработчика.

Почему Django REST Framework

DRF предлагает убедительные преимущества:

  1. Быстрая разработка: viewsets и routers минимизируют шаблонный код
  2. Сериализация: преобразование сложных типов данных в нативные Python и обратно
  3. Аутентификация: поддержка Token, session, JWT и OAuth
  4. Browsable API: интерактивная документация бесплатно
  5. Throttling: встроенное ограничение частоты запросов

Настройка и установка

Создание проекта Django

# Создайте виртуальное окружение
python -m venv venv
source venv/bin/activate # Linux/macOS
# venv\Scripts\activate # Windows
# Установите Django и DRF
pip install django djangorestframework
pip install django-filter # Для фильтрации
# Создайте проект и приложение
django-admin startproject myproject
cd myproject
python manage.py startapp api

Настройка параметров

myproject/settings.py:

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Сторонние
'rest_framework',
'django_filters',
# Локальные
'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',
],
}

Модели

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']

Выполните миграции:

python manage.py makemigrations
python manage.py migrate

Сериализаторы

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 и 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()
# Неаутентифицированные пользователи видят только опубликованные посты
if not self.request.user.is_authenticated:
queryset = queryset.filter(status='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

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
]

Аутентификация

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 = [
# ... существующие маршруты
path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]

Использование:

# Получить токены
curl -X POST http://localhost:8000/api/token/ \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"password"}'
# Использовать access token
curl http://localhost:8000/api/posts/ \
-H "Authorization: Bearer <access_token>"

Фильтрация и поиск

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']

Использование в viewset:

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

Тестирование

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)

Запуск тестов:

python manage.py test api

Ключевые выводы

  1. Используйте ViewSets: минимизируйте повторяющийся код для CRUD-операций
  2. Пользовательские permissions: реализуйте бизнес-логику в классах permissions
  3. Оптимизируйте запросы: используйте selectrelated и prefetchrelated
  4. Разные serializers: для list и detail views нужны разные данные
  5. Filter backends: включайте мощные возможности запросов без пользовательского кода
  6. Тщательно тестируйте: API-тесты рано выявляют проблемы интеграции

Django REST Framework предоставляет всё необходимое для надёжных и безопасных API, сохраняя продуктивность, которой известен Django.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
Зетка Интерактив