APIView, generics и ViewSet

Три уровня абстракции для эндпоинтов DRF — от ручного APIView до ViewSet с автоматической маршрутизацией.

Вьюха DRF — это объект, который принимает request, выбирает сериализатор и набор данных и возвращает Response. DRF предлагает три уровня: APIView (всё руками), generic-вьюхи (типовые операции готовы) и ViewSet (несколько операций в одном классе + роутер).

Зачем это знать на практике

Выбор уровня абстракции определяет, сколько кода вы напишете и сколько потом будете поддерживать. Возьмёте слишком низкий уровень — утонете в одинаковых get/post для каждого ресурса. Возьмёте слишком высокий, не понимая его, — не сможете вставить нестандартную логику и будете воевать с фреймворком. Поэтому полезно знать все три уровня и осознанно спускаться или подниматься по лестнице абстракции под конкретную задачу.

APIView: полный контроль

APIView — это DRF-надстройка над Django-View: методы называются по HTTP-глаголам, на вход даётся разобранный request.data, на выход — Response (DRF сам отрендерит его в JSON).

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status

class ArticleList(APIView):
    def get(self, request):
        articles = Article.objects.all()
        data = ArticleSerializer(articles, many=True).data
        return Response(data)

    def post(self, request):
        serializer = ArticleSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        serializer.save()
        return Response(serializer.data, status=status.HTTP_201_CREATED)

Видно, что для каждого ресурса этот код почти одинаков. Именно эту рутину и устраняют следующие уровни.

Generic-вьюхи: типовые операции готовы

Generic-вьюхи закрывают стандартный CRUD. Достаточно указать queryset и serializer_class — а нужное поведение даёт выбор базового класса:

from rest_framework import generics

class ArticleList(generics.ListCreateAPIView):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer

class ArticleDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer

Две короткие вьюхи дают полноценный API: список + создание и получение + изменение + удаление одной записи. Имена классов читаются как список операций:

КлассHTTPОперация
ListCreateAPIViewGET, POSTсписок, создание
RetrieveUpdateDestroyAPIViewGET, PUT/PATCH, DELETEчтение, обновление, удаление одной записи
ListAPIViewGETтолько список (read-only)

Когда нужна нестандартная логика, переопределяют хук, а не весь метод. Например, подставить текущего пользователя в автора при создании:

class ArticleList(generics.ListCreateAPIView):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer

    def perform_create(self, serializer):
        serializer.save(author=self.request.user)

ViewSet и роутеры

Даже две generic-вьюхи на ресурс повторяют queryset и serializer_class. ModelViewSet собирает весь CRUD в один класс:

from rest_framework import viewsets

class ArticleViewSet(viewsets.ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer

В ViewSet методы называются по действиям, а не по HTTP-глаголам: list, create, retrieve, update, partial_update, destroy. ModelViewSet реализует их все. Маршруты для них генерирует роутер — не нужно вручную прописывать каждый URL:

from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register(r"articles", ArticleViewSet)

urlpatterns = router.urls
# создаст:
# GET/POST          /articles/
# GET/PUT/PATCH/DEL /articles/{pk}/

Своё действие добавляют декоратором @action — роутер сам сделает для него URL:

from rest_framework.decorators import action

class ArticleViewSet(viewsets.ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer

    @action(detail=True, methods=["post"])
    def publish(self, request, pk=None):
        article = self.get_object()
        article.is_published = True
        article.save()
        return Response({"status": "опубликовано"})
# даст POST /articles/{pk}/publish/

Что выбрать

  • APIView — когда логика нетиповая: эндпоинт не вокруг одной модели, своя обработка, сторонний сервис.
  • Generic-вьюхи — когда нужен обычный CRUD, но хочется явно видеть, какие операции у ресурса, и точечно настраивать каждую.
  • ViewSet + роутер — когда ресурсов много и они однотипные: меньше всего кода, единые URL по соглашению.

Полезно помнить: это одна лестница. ViewSet построен на generic-миксинах, а те — на APIView. Поэтому переопределять хуки (get_queryset, perform_create) можно на любом уровне одинаково.

Как это работает под капотом

В основе всего лежит APIView.dispatch(). Он оборачивает Django-request в DRF-Request.data и согласованием контента), запускает аутентификацию, проверку прав и throttling, а затем по HTTP-методу вызывает нужный обработчик. Generic-вьюхи добавляют миксины (ListModelMixin, CreateModelMixin и т.д.): каждый миксин — это готовый метод вроде list(), опирающийся на get_queryset() и get_serializer(). ViewSet отличается тем, что не привязывает методы к HTTP-глаголам сразу: роутер при регистрации вызывает as_view({"get": "list", "post": "create"}) и так строит соответствие «URL + метод → действие». Поэтому один и тот же ViewSet обслуживает и список, и деталь — роутер просто создаёт две точки входа с разными отображениями.

Частые ошибки

  • Возвращать обычный Django JsonResponse из APIView. Теряется согласование контента и DRF-рендеринг; нужен rest_framework.response.Response.
  • Переопределять весь метод ради мелочи. Чтобы подставить пользователя или отфильтровать выборку, есть хуки perform_create() и get_queryset().
  • Прописывать URL для ViewSet руками. Без роутера придётся вручную раскладывать действия по методам — теряется весь смысл ViewSet.
  • Путать действия и HTTP-глаголы. В ViewSet метод называется create, а не post; в APIView — наоборот, post.
  • Забыть basename при router.register без queryset. Если queryset не задан атрибутом, роутер не выведет имя маршрута и упадёт.

Итоги

  • APIView — методы по HTTP-глаголам, максимум контроля, максимум ручного кода.
  • Generic-вьюхи (ListCreateAPIView и др.) дают CRUD по queryset + serializer_class; тонкая настройка — через хуки.
  • ModelViewSet объединяет все действия в один класс; URL генерирует роутер.
  • Свои действия в ViewSet добавляют декоратором @action.
  • Все три уровня — одна лестница абстракции на базе APIView; хуки переопределяются одинаково.
Проверьте себя
1. Какой класс одной строкой даст и список с созданием, и не потребует писать get/post вручную?
AAPIView
Bgenerics.ListCreateAPIView
Cviewsets.ViewSet без миксинов
Ddjango.views.View
2. Как в ViewSet называется метод, обрабатывающий POST-создание объекта?
Apost
Bcreate
Cadd
Dnew
3. Зачем при работе с ViewSet нужен роутер (DefaultRouter)?
AОн валидирует входные данные вместо сериализатора
BОн автоматически строит URL-маршруты для действий ViewSet
CОн кеширует ответы
DОн заменяет сериализатор