🚀 Введение в Django Rest Framework: полный гайд для бэкенд-разработчика¶
TL;DR: Django Rest Framework (DRF) — мощный и гибкий тулкит для создания REST API на базе Django. После прочтения вы сможете: настроить сериализаторы, создать ViewSets с роутингом, добавить аутентификацию по JWT и написать тесты. Актуально для Django 5.2 + DRF 3.16 (2026).
🔍 Почему DRF, а не писать «голый» Django или FastAPI?¶
| Критерий | DRF | «Голый» Django | FastAPI |
|---|---|---|---|
| Сериализация | ✅ Встроенная, с валидацией | ❌ Ручная (json()) | ✅ Pydantic |
| Browsable API | ✅ Интерактивный UI в браузере | ❌ Нет | ❌ Только Swagger |
| Аутентификация | ✅ Плагины: JWT, OAuth2, Session | ⚠️ Только Session | ⚠️ Требует настройки |
| Permissions | ✅ Гибкая система прав доступа | ❌ Ручная проверка | ❌ Ручная |
| Pagination/Filtering | ✅ Встроенные классы | ❌ Ручная реализация | ⚠️ Через зависимости |
| Документация | ✅ Auto OpenAPI (drf-spectacular) | ❌ Ручная | ✅ Auto OpenAPI |
| Экосистема | ✅ 8000+ пакетов Django | ✅ Огромная | 🔄 Растущая |
DRF идеален, когда:
- Вы уже используете Django для бэкенда
- Нужен админ-панель + API в одном проекте
- Требуется быстрая разработка с минимумом бойлерплейта
- Команда знает Django и хочет единый стек
📋 Архитектура: как DRF обрабатывает запрос¶
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Клиент │ │ Django + DRF │ │ База данных │
│ (браузер/моб) │ │ (backend) │ │ (PostgreSQL) │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
│ 1. HTTP-запрос │ │
│ (GET/POST/PUT/DEL) │ │
│──────────────────────>│ │
│ │ │
│ │ 2. Middleware: │
│ │ - Auth │
│ │ - Throttling │
│ │ - CORS │
│ │ │
│ │ 3. Router → ViewSet │
│ │──────────────────────>│
│ │ │
│ │ 4. Serializer: │
│ │ - Валидация │
│ │ - Преобразование │
│ │ │
│ │ 5. ORM-запрос │
│ │──────────────────────>│
│ │ │
│ │ 6. Ответ в JSON │
│<──────────────────────│ │
Ключевой момент: вся логика преобразования данных (ORM ↔ JSON) инкапсулирована в серриализаторах — это сердце DRF.
🚀 Быстрый старт: 10 минут до первого эндпоинта¶
Шаг 1: Установка и настройка проекта¶
# Создаём виртуальное окружение
python3.13 -m venv venv
source venv/bin/activate
# Устанавливаем зависимости
pip install django==5.2 djangorestframework==3.16.1
pip install django-filter==24.3 djangorestframework-simplejwt==5.4.0
pip install drf-spectacular==0.28.0 # для авто-документации
# Создаём проект и приложение
django-admin startproject config .
python manage.py startapp api
Шаг 2: Регистрация в settings.py¶
# config/settings.py
INSTALLED_APPS = [
# ... стандартные Django apps ...
'rest_framework',
'rest_framework_simplejwt',
'django_filters',
'drf_spectacular',
'api', # ваше приложение
]
# Настройки DRF
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
'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',
],
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}
# Настройки JWT
from datetime import timedelta
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30),
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
'ROTATE_REFRESH_TOKENS': True,
}
Шаг 3: Модель + Миграции¶
# api/models.py
from django.db import models
from django.conf import settings
class Task(models.Model):
class Status(models.TextChoices):
TODO = 'todo', 'To Do'
IN_PROGRESS = 'in_progress', 'In Progress'
DONE = 'done', 'Done'
title = models.CharField(max_length=255)
description = models.TextField(blank=True)
status = models.CharField(
max_length=20,
choices=Status.choices,
default=Status.TODO,
)
priority = models.IntegerField(default=1) # 1-5
owner = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='tasks',
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created_at']
indexes = [
models.Index(fields=['owner', 'status']),
]
def __str__(self):
return self.title
# Применяем миграции
python manage.py makemigrations
python manage.py migrate
🔑 Сериализаторы: сердце DRF¶
Сериализаторы преобразуют модели Django ↔ JSON и обеспечивают валидацию данных.
Базовый сериализатор¶
# api/serializers.py
from rest_framework import serializers
from .models import Task
class TaskSerializer(serializers.ModelSerializer):
# Вычисляемое поле (read-only)
is_overdue = serializers.SerializerMethodField()
class Meta:
model = Task
fields = [
'id', 'title', 'description', 'status',
'priority', 'owner', 'created_at', 'updated_at', 'is_overdue'
]
read_only_fields = ['owner', 'created_at', 'updated_at']
def get_is_overdue(self, obj):
# Пример логики: задача просрочена, если статус не DONE и дедлайн прошёл
return False # заглушка, можно добавить поле due_date
def validate_priority(self, value):
"""Кастомная валидация: приоритет от 1 до 5"""
if not 1 <= value <= 5:
raise serializers.ValidationError("Priority must be between 1 and 5")
return value
🔥 Продвинутые паттерны сериализаторов¶
📝 Разные сериализаторы для чтения и записи
# Для создания/обновления — меньше полей, строже валидация
class TaskWriteSerializer(serializers.ModelSerializer):
class Meta:
model = Task
fields = ['title', 'description', 'status', 'priority']
def validate_title(self, value):
if len(value.strip()) < 3:
raise serializers.ValidationError("Title too short")
return value
# Для чтения — больше данных, включая вложенные
class TaskReadSerializer(serializers.ModelSerializer):
owner = serializers.StringRelatedField(read_only=True)
is_overdue = serializers.SerializerMethodField()
class Meta:
model = Task
fields = '__all__'
def get_is_overdue(self, obj):
# реальная логика проверки дедлайна
return False
# В ViewSet выбираем сериализатор динамически
def get_serializer_class(self):
if self.action in ['create', 'update', 'partial_update']:
return TaskWriteSerializer
return TaskReadSerializer
🔗 Вложенные сериализаторы (Nested)
# Если нужно возвращать данные владельца
class OwnerSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email']
class TaskSerializer(serializers.ModelSerializer):
owner = OwnerSerializer(read_only=True)
class Meta:
model = Task
fields = '__all__'
read_only_fields = ['owner']
👁️ Views и ViewSets: где живёт бизнес-логика¶
Простой пример: APIView¶
# api/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from .models import Task
from .serializers import TaskSerializer
class TaskListCreateView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request):
# Фильтруем задачи по владельцу
tasks = Task.objects.filter(owner=request.user)
serializer = TaskSerializer(tasks, many=True)
return Response(serializer.data)
def post(self, request):
serializer = TaskSerializer(data=request.data)
if serializer.is_valid():
# Автоматически проставляем владельца
serializer.save(owner=request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
🔥 ViewSets + Routers: меньше кода, больше возможностей¶
# api/views.py
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import Task
from .serializers import TaskSerializer, TaskReadSerializer, TaskWriteSerializer
from .filters import TaskFilter # см. ниже
class TaskViewSet(viewsets.ModelViewSet):
"""
Полный CRUD для задач с фильтрацией и кастомными экшенами.
"""
serializer_class = TaskSerializer
filterset_class = TaskFilter
search_fields = ['title', 'description']
ordering_fields = ['created_at', 'priority', 'status']
def get_queryset(self):
# Критически важно: каждый пользователь видит только свои задачи
return Task.objects.filter(
owner=self.request.user
).select_related('owner') # оптимизация: убираем N+1 запрос
def get_serializer_class(self):
if self.action in ['create', 'update', 'partial_update']:
return TaskWriteSerializer
return TaskReadSerializer
def perform_create(self, serializer):
# Автоматически проставляем владельца при создании
serializer.save(owner=self.request.user)
# 🔥 Кастомный экшен: статистика по задачам пользователя
@action(detail=False, methods=['get'])
def stats(self, request):
queryset = self.get_queryset()
return Response({
'total': queryset.count(),
'by_status': {
status: queryset.filter(status=status).count()
for status, _ in Task.Status.choices
},
'high_priority': queryset.filter(priority__gte=4).count(),
})
# 🔥 Кастомный экшен: отметить задачу как выполненную
@action(detail=True, methods=['post'])
def complete(self, request, pk=None):
task = self.get_object()
task.status = Task.Status.DONE
task.save(update_fields=['status', 'updated_at'])
serializer = self.get_serializer(task)
return Response(serializer.data)
🔗 Роутинг: автоматическая генерация URL¶
# api/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import TaskViewSet
router = DefaultRouter()
router.register(r'tasks', TaskViewSet, basename='task')
urlpatterns = [
path('', include(router.urls)),
]
# config/urls.py
from django.contrib import admin
from django.urls import path, include
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
from drf_spectacular.views import (
SpectacularAPIView,
SpectacularSwaggerView,
)
urlpatterns = [
path('admin/', admin.site.urls),
# Auth endpoints
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
# API v1
path('api/v1/', include('api.urls')),
# Auto-generated docs
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
]
Что генерирует Router [[5]]:
| HTTP Method | URL Pattern | ViewSet Method |
|---|---|---|
| GET | /api/v1/tasks/ |
list() |
| POST | /api/v1/tasks/ |
create() |
| GET | /api/v1/tasks/{id}/ |
retrieve() |
| PUT | /api/v1/tasks/{id}/ |
update() |
| PATCH | /api/v1/tasks/{id}/ |
partial_update() |
| DELETE | /api/v1/tasks/{id}/ |
destroy() |
| GET | /api/v1/tasks/stats/ |
stats() (custom action) |
| POST | /api/v1/tasks/{id}/complete/ |
complete() (custom action) |
🔐 Аутентификация и права доступа¶
Базовая настройка JWT¶
# settings.py (см. выше) + добавляем в urls:
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
urlpatterns += [
path('api/token/', TokenObtainPairView.as_view()),
path('api/token/refresh/', TokenRefreshView.as_view()),
]
Тестирование авторизации через curl¶
# 1. Получаем токен
curl -X POST http://localhost:8000/api/token/ \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"yourpass"}'
# Ответ:
# {"access":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...","refresh":"..."}
# 2. Используем токен в запросах
curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
http://localhost:8000/api/v1/tasks/
🔥 Кастомные права доступа (Permissions)¶
# api/permissions.py
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
"""
Объект может редактировать только владелец, остальные — только чтение.
"""
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 allowed only to owner
return obj.owner == request.user
# Использование в ViewSet:
class TaskViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
# ...
🧪 Тестирование API: обязательно для production¶
DRF предоставляет APITestCase и APIClient для удобного тестирования.
# api/tests.py
from django.contrib.auth.models import User
from rest_framework.test import APITestCase, APIClient
from rest_framework import status
from .models import Task
class TaskAPITest(APITestCase):
def setUp(self):
self.client = APIClient()
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.client.force_authenticate(user=self.user)
self.task = Task.objects.create(
title='Test Task',
owner=self.user,
priority=3
)
def test_list_tasks(self):
response = self.client.get('/api/v1/tasks/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data['results']), 1)
def test_create_task(self):
data = {
'title': 'New Task',
'description': 'Test description',
'priority': 5
}
response = self.client.post('/api/v1/tasks/', data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Task.objects.count(), 2)
self.assertEqual(Task.objects.last().owner, self.user)
def test_cannot_access_other_user_task(self):
# Создаём задачу от имени другого пользователя
other_user = User.objects.create_user(username='other', password='pass')
other_task = Task.objects.create(title='Other', owner=other_user)
# Пытаемся получить чужую задачу
response = self.client.get(f'/api/v1/tasks/{other_task.id}/')
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
# 🔥 Важно: возвращаем 404, а не 403 — не раскрываем существование объекта
Запуск тестов:
python manage.py test api -v2
🚨 Частые ошибки и как их избежать¶
❌ «N+1 запросов» при сериализации связанных объектов¶
Проблема: при выводе списка задач с данными владельца — отдельный запрос к БД на каждую задачу.
Решение: используйте select_related() для ForeignKey и prefetch_related() для ManyToMany [[11]].
def get_queryset(self):
return Task.objects.filter(
owner=self.request.user
).select_related('owner') # ✅ один JOIN вместо N запросов
❌ «Данные одного пользователя видны другому»¶
Проблема: забыли фильтровать queryset по владельцу.
Решение: всегда переопределяйте get_queryset() и добавляйте фильтр owner=self.request.user [[18]].
❌ «Сериализатор принимает лишние поля»¶
Проблема: клиент может передать поля, которых нет в модели, и они будут проигнорированы без ошибки.
Решение: используйте extra_kwargs или явно перечисляйте fields в Meta, а не __all__ в production.
class Meta:
model = Task
fields = ['id', 'title', 'status'] # ✅ явно
extra_kwargs = {
'owner': {'read_only': True},
}
❌ «Токен JWT протух, а пользователь не понимает почему»¶
Проблема: клиент получает 401, но не знает, что нужно обновить токен.
Решение: на фронтенде обрабатывайте 401 → вызывайте /token/refresh/ → повторяйте исходный запрос.
✅ Чеклист перед деплоем¶
- [ ]
DEBUG = Falseв production-настройках - [ ]
ALLOWED_HOSTSнастроен под ваши домены - [ ] Секреты (
SECRET_KEY,DB_PASSWORD) вынесены в environment variables - [ ] В
REST_FRAMEWORK['DEFAULT_PERMISSION_CLASSES']стоитIsAuthenticated(или кастомные права) - [ ] Все эндпоинты покрыты тестами (
python manage.py test) - [ ] Добавлена пагинация (
PAGE_SIZE) для списковых эндпоинтов - [ ] Настроен
django-filterдля сложных запросов - [ ] Включена генерация OpenAPI-схемы (
drf-spectacular) - [ ] Логируются ошибки 4xx/5xx (через middleware или logging)
- [ ] База данных — PostgreSQL (не SQLite) в production
🎯 Итог: что вы получаете с DRF¶
✅ Для разработчика:
- Минимум бойлерплейта: CRUD за 10 строк кода
- Browsable API для отладки прямо в браузере
- Встроенная валидация и сериализация
- Готовые решения для аутентификации, пагинации, фильтрации
✅ Для команды:
- Единый стандарт описания эндпоинтов (OpenAPI)
- Авто-документация (Swagger/ReDoc)
- Легко тестировать через APIClient
✅ Для бизнеса:
- Быстрая итерация: новые эндпоинты за часы, не дни
- Безопасность «из коробки»: permissions, throttling, CORS
- Масштабируемость: работает на том же стеке, что и основной Django-проект
🔗 Полезные ссылки¶
- Официальная документация DRF
- Authentication & Permissions Guide [[18]]
- Serializers Best Practices (TestDriven.io) [[11]]
- DRF Examples на GitHub
- drf-spectacular для OpenAPI 3.0
💡 Pro tip: Если вы используете
ModelViewSet, но вам нужна кастомная логика для одного метода — не бойтесь переключиться на@actionили даже наAPIViewдля этого конкретного эндпоинта. Гибкость — сильная сторона DRF.
Вопрос на засыпку: какой паттерн вы чаще используете — APIView для полного контроля или ViewSet для скорости? Делитесь в комментариях 👇
Статья обновлена: май 2026. Актуально для Django 5.2 LTS + DRF 3.16.1.