Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. DRF: Views & ViewSets
drf_views

DRF: Views & ViewSets

APIView, GenericAPIView, ViewSet, Router, pagination, filtering

Django REST Framework: views, ViewSet и router

Статус: актуально для Django 5.2/6.0 и современных версий Django REST Framework.

View принимает HTTP-запрос, выбирает допустимые объекты, запускает аутентификацию и permissions, вызывает сериализатор и формирует Response. DRF предлагает несколько уровней абстракции, но более короткий класс не означает меньшую ответственность: защита от доступа к чужим объектам, стоимость queryset и корректность записи остаются задачей приложения.

Практическое правило выбора:

  • APIView подходит для команды или отчёта, которые плохо описываются CRUD модели;
  • generic views удобны для одного стандартного списка или объекта;
  • ViewSet с router уменьшает повторение в ресурсе с несколькими CRUD-действиями.

Выбирайте форму по смыслу endpoint, а не по принципу «ViewSet всегда лучше».

#Что добавляет APIView

APIView расширяет обычную 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().

#Generic views

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() обеспечивает корректную работу на каждом запросе и позволяет учитывать пользователя.

#ModelViewSet

ModelViewSet собирает действия 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 или сервисный слой.

#Permissions и объектный доступ

DRF сначала вызывает has_permission(). Для детального объекта стандартный get_object() затем вызывает has_object_permission(). Есть два важных следствия:

  1. object permission не фильтрует список — get_queryset() должен сам исключить недоступные строки;
  2. собственный 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

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() либо другой осознанный способ разрешения гонки.

#Legacy и актуальные ориентиры

  • Function-based views с @api_view по-прежнему поддерживаются; это не устаревший API, а ещё один подход для маленького endpoint.
  • Ручная проверка if request.user.is_authenticated внутри каждого action работает, но хуже декларативных permissions и легче пропускается.
  • Один глобальный Book.objects.all() в учебном CRUD годится только для полностью публичных данных. В прикладном API queryset почти всегда является частью модели доступа.
  • ModelViewSet не обязан соответствовать одной таблице. Он представляет HTTP-ресурс, а бизнес-операция может затронуть несколько моделей через сервисный слой.

Хорошая DRF view остаётся короткой не потому, что проверки удалены, а потому, что каждая ответственность видна: queryset ограничивает область данных, permissions определяют действие, сериализатор проверяет контракт, сервис выполняет бизнес-операцию.

Далее: DRF: Аутентификация