APIView, GenericAPIView, ViewSet, Router, pagination, filtering
Статус: актуально для Django 5.2/6.0 и современных версий Django REST Framework.
View принимает HTTP-запрос, выбирает допустимые объекты, запускает аутентификацию и permissions, вызывает сериализатор и формирует Response. DRF предлагает несколько уровней абстракции, но более короткий класс не означает меньшую ответственность: защита от доступа к чужим объектам, стоимость queryset и корректность записи остаются задачей приложения.
Практическое правило выбора:
APIView подходит для команды или отчёта, которые плохо описываются CRUD модели;ViewSet с router уменьшает повторение в ресурсе с несколькими CRUD-действиями.Выбирайте форму по смыслу endpoint, а не по принципу «ViewSet всегда лучше».
APIViewAPIView расширяет обычную Django view: преобразует запрос в DRF Request, выбирает parser и renderer, выполняет аутентификацию, permissions и throttling, а исключения DRF переводит в согласованный API-ответ.
from django.shortcuts import get_object_or_404
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
from library.models import Book
from library.serializers import BookSerializer
class BookDetail(APIView):
def get(self, request, pk):
queryset = Book.objects.filter(tenant=request.tenant)
book = get_object_or_404(queryset, pk=pk)
serializer = BookSerializer(book, context={"request": request})
return Response(serializer.data)
def delete(self, request, pk):
queryset = Book.objects.filter(tenant=request.tenant)
book = get_object_or_404(queryset, pk=pk)
book.delete()
return Response(status=status.HTTP_204_NO_CONTENT)Пример намеренно начинает поиск с queryset текущего арендатора. Если сначала получить Book.objects.get(pk=pk), а права проверить позднее или забыть, endpoint станет источником IDOR — доступа к объекту по угаданному идентификатору.
Для полноценного endpoint здесь всё равно нужны permission_classes. Если permission реализует has_object_permission(), при ручном получении объекта вызовите self.check_object_permissions(request, book). Generic views делают это в своём get_object().
GenericAPIView добавляет соглашения queryset, serializer_class, get_queryset(), get_object() и get_serializer(). Конкретные классы объединяют их с mixin:
from rest_framework import generics
from rest_framework.permissions import IsAuthenticatedOrReadOnly
class BookList(generics.ListCreateAPIView):
serializer_class = BookSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
def get_queryset(self):
return (
Book.objects.filter(tenant=self.request.tenant)
.select_related("author")
.order_by("-created_at", "-pk")
)
def perform_create(self, serializer):
serializer.save(
tenant=self.request.tenant,
created_by=self.request.user,
)
class BookDetail(generics.RetrieveUpdateDestroyAPIView):
serializer_class = BookSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
def get_queryset(self):
return Book.objects.filter(tenant=self.request.tenant)get_queryset() — не просто место для фильтра из query string. Это важная граница доступа. Базовое ограничение по организации, владельцу и видимости должно применяться ко всем действиям до поиска объекта.
Не вычисляйте queryset один раз в атрибуте и не обращайтесь к self.queryset напрямую в прикладном коде: get_queryset() обеспечивает корректную работу на каждом запросе и позволяет учитывать пользователя.
ModelViewSetModelViewSet собирает действия list, create, retrieve, update, partial_update и destroy. Он хорош для обычного ресурса, если доступ ко всем действиям образует понятную политику.
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from blog.models import Post
from blog.serializers import (
PostDetailSerializer,
PostListSerializer,
PostWriteSerializer,
)
class PostViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticatedOrReadOnly]
def get_queryset(self):
queryset = Post.objects.filter(tenant=self.request.tenant)
if self.action == "list":
queryset = queryset.filter(status=Post.Status.PUBLISHED)
return queryset.select_related("author", "category")
def get_serializer_class(self):
if self.action == "list":
return PostListSerializer
if self.action in {"create", "update", "partial_update"}:
return PostWriteSerializer
return PostDetailSerializer
def perform_create(self, serializer):
serializer.save(
tenant=self.request.tenant,
author=self.request.user,
)Этот пример показывает механизмы, но политика видимости требует внимания: автору может понадобиться доступ к собственному черновику в retrieve, а обычному читателю — нет. Такое правило лучше выразить отдельным queryset/policy и покрыть тестами по ролям и действиям.
Не принимайте tenant и author от клиента, если их определяет сервер. И наоборот, serializer.save(author=request.user) не проверяет автоматически права на остальные связи — это делают scoped related querysets, permissions или сервисный слой.
DRF сначала вызывает has_permission(). Для детального объекта стандартный get_object() затем вызывает has_object_permission(). Есть два важных следствия:
get_queryset() должен сам исключить недоступные строки;get_object() обязан вызвать check_object_permissions() вручную.Также проверяйте не только retrieve. Утечки часто остаются в update, partial_update, destroy и custom action. Лучший тест безопасности параметризует один чужой объект по всем таким действиям.
Декоратор @action добавляет маршрут к ViewSet. Разрешение действия лучше задавать декларативно, а изменение состояния — передавать сервисной функции.
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
class PostViewSet(viewsets.ModelViewSet):
# Остальная конфигурация опущена.
@action(
detail=True,
methods=["post"],
permission_classes=[IsAuthenticated, CanPublishPost],
)
def publish(self, request, pk=None):
post = self.get_object()
post = publish_post(post=post, actor=request.user)
return Response(PostDetailSerializer(post).data)self.get_object() здесь сохраняет scoped queryset и object permissions. Сервис publish_post() может проверять допустимый переход состояния, выполнять транзакцию и планировать событие через transaction.on_commit().
Для «лайка» полезно выбрать идемпотентный контракт. Например, PUT /posts/{id}/like/ означает «лайк существует», а DELETE — «лайка нет». Повтор запроса тогда не переключает состояние неожиданно.
Router связывает действия ViewSet с URL и именами маршрутов:
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from blog.views import PostViewSet
router = DefaultRouter()
router.register("posts", PostViewSet, basename="post")
urlpatterns = [
path("api/", include(router.urls)),
]Получатся, среди прочих, имена post-list, post-detail и post-publish. DefaultRouter дополнительно создаёт корень API, а SimpleRouter — нет. Если у ViewSet отсутствует статический queryset, явно передайте basename, иначе router часто не сможет вывести его из модели.
Router не заменяет проектирование URL. Не превращайте каждую бизнес-команду в случайный custom action только потому, что декоратор удобен.
Для стандартных параметров можно совместить несколько backend. Они указываются одним списком — повторное присваивание filter_backends затрёт предыдущее.
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import OrderingFilter, SearchFilter
class PostViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = PostListSerializer
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = ["category", "status"]
search_fields = ["title", "body"]
ordering_fields = ["created_at", "title"]
ordering = ["-created_at", "-pk"]
def get_queryset(self):
return Post.objects.filter(tenant=self.request.tenant)Установите и подключите django-filter, если используете DjangoFilterBackend. ordering_fields должен быть allowlist: значение "__all__" может раскрыть существование служебных полей и разрешить дорогие сортировки. Сначала ограничивают базовый queryset, затем применяют пользовательские фильтры.
Сложные фильтры лучше описать FilterSet: там видны типы, диапазоны и методы. Никогда не подставляйте имя поля из query string в raw SQL.
Глобальная настройка подходит большинству списков:
REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": (
"rest_framework.pagination.PageNumberPagination"
),
"PAGE_SIZE": 20,
}PageNumberPagination понятна пользователю, но большие смещения и COUNT(*) могут быть дорогими. LimitOffsetPagination удобна для произвольного смещения и имеет те же ограничения. CursorPagination лучше держит последовательность при вставках между запросами, но требует стабильной, практически уникальной сортировки — например ("-created_at", "-pk") — и не даёт перейти на произвольную страницу.
Если разрешаете клиенту менять размер страницы, обязательно задайте максимум. Пагинация ограничивает размер ответа, но сама по себе не устраняет N+1.
Используйте serializer.is_valid(raise_exception=True), get_object_or_404() и исключения DRF вместо десятков вручную собранных ответов. Так формат ошибок и работа глобального exception handler остаются единообразными.
ATOMIC_REQUESTS или transaction.atomic() не делают конкурентное обновление автоматически безопасным. Для операций над остатком, балансом или переходом состояния нужны ограничения базы, условный UPDATE, select_for_update() либо другой осознанный способ разрешения гонки.
@api_view по-прежнему поддерживаются; это не устаревший API, а ещё один подход для маленького endpoint.if request.user.is_authenticated внутри каждого action работает, но хуже декларативных permissions и легче пропускается.Book.objects.all() в учебном CRUD годится только для полностью публичных данных. В прикладном API queryset почти всегда является частью модели доступа.ModelViewSet не обязан соответствовать одной таблице. Он представляет HTTP-ресурс, а бизнес-операция может затронуть несколько моделей через сервисный слой.Хорошая DRF view остаётся короткой не потому, что проверки удалены, а потому, что каждая ответственность видна: queryset ограничивает область данных, permissions определяют действие, сериализатор проверяет контракт, сервис выполняет бизнес-операцию.
Далее: DRF: Аутентификация