🚀 Введение в 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']
> ⚠️ **Осторожно с `depth`**: `depth = 1` автоматически сериализует связанные модели, но вы теряете контроль над полями. Лучше явное определение.

👁️ 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-проект


🔗 Полезные ссылки


💡 Pro tip: Если вы используете ModelViewSet, но вам нужна кастомная логика для одного метода — не бойтесь переключиться на @action или даже на APIView для этого конкретного эндпоинта. Гибкость — сильная сторона DRF.

Вопрос на засыпку: какой паттерн вы чаще используете — APIView для полного контроля или ViewSet для скорости? Делитесь в комментариях 👇


Статья обновлена: май 2026. Актуально для Django 5.2 LTS + DRF 3.16.1.

← Все публикации