Filter concepts by levelShowing all levels.

Django · DRF Views

DRF Views

APIView, generic views and mixins, ViewSets and Routers, and the customization hook methods

  • APIView → a mixin → a generic viewa spectrum of how much CRUD behavior is pre-wired versus hand-written
  • A ViewSet organizes by RESOURCE, not URLone class, actions named list/create/retrieve/update/destroy — a Router generates the URLs
  • Customize via the narrowest hook methodget_queryset()/get_serializer_class()/perform_create() — never reimplement list()/create() wholesale
  • A server-controlled field belongs in perform_create()never accepted as writable serializer input — that's a real authorization bypass

1.APIView, generic views, and mixins

APIView is DRF's equivalent of Django's View — the base class every other DRF class-based view builds on, adding DRF-specific request/response handling (request.data instead of request.POST, content negotiation, DRF's own authentication/permission/throttle checks) but leaving get()/post()/etc. entirely up to you, same as plain Django. Generic views (ListAPIView, RetrieveAPIView, CreateAPIView, etc.) are pre-built APIView subclasses that already implement the common CRUD patterns via mixins — each generic view is really just a specific mixin combination plus a matching HTTP-method handler. Mixins (ListModelMixin, CreateModelMixin, RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin) are the actual reusable behavior — a .list()/.create()/.retrieve()/.update()/.destroy() method each — that generic views compose together, and that a custom view can mix in selectively for a non-standard combination.

Key features

  • APIView.dispatch() is what actually runs DRF's authentication/permission/throttle checks before calling get()/post()/etc. — a plain Django View subclass calling these methods manually would skip that machinery entirely
  • request.data (not request.POST) is DRF's parsed-body access on an APIView — it works for JSON, form data, and multipart alike, unlike request.POST, which only handles form-encoded/multipart bodies
  • Each generic view mixes GenericAPIView (queryset/serializer_class handling, pagination hooks, filtering hooks) with one or more of the five model mixins — ListAPIView = ListModelMixin + GenericAPIView with get() calling self.list()
  • ListCreateAPIView and RetrieveUpdateDestroyAPIView are the two most commonly used COMBINED generic views — list+create on a collection endpoint, retrieve+update+delete on a detail endpoint, matching the conventional URL pattern of /items/ vs /items/<pk>/
  • Each mixin method (list(), create(), retrieve(), update(), destroy()) internally calls get_queryset()/get_object()/get_serializer() — overriding one of THOSE hook methods is usually the correct way to customize behavior, rather than overriding list()/create() themselves
  • UpdateModelMixin.update() handles both PUT (full update) and PATCH (partial update, via partial=True passed to the serializer) through the same method — partial is determined from the request method, not a separate mixin

The view spectrum, least to most generated

The view spectrum, least to most generated
BaseProvides
APIViewDRF request/response/auth plumbing only — every handler method hand-written
A mixin + GenericAPIViewone specific behavior (.list(), .create(), ...) — handler methods still wired manually
A generic view (ListAPIView, etc.)the full, common combination already wired — no manual handler methods needed
How much DRF generates, least to most

APIView

request/response/auth plumbing only — every handler hand-written

A mixin + GenericAPIView

one behavior (.list(), .create()) — handlers still wired manually

A generic view (ListAPIView, ...)

the full common combination already wired

  1. APIView — request/response/auth plumbing only — every handler hand-written
  2. A mixin + GenericAPIView — one behavior (.list(), .create()) — handlers still wired manually
  3. A generic view (ListAPIView, ...) — the full common combination already wired

Command

python
class MyView(generics.ListCreateAPIView):
    queryset = Model.objects.all()
    serializer_class = MySerializer

APIView gives DRF's request/response/auth plumbing with every handler hand-written; a mixin gives one reusable behavior (.list()/.create()/etc.) still needing manual wiring; a generic view gives the full common combination pre-wired. Customize a generic view via its narrower hook methods (get_queryset(), get_serializer(), get_object()), not by overriding list()/create() wholesale. Always use request.data, never request.POST, on a DRF view.

2.ViewSet, GenericViewSet, ModelViewSet, routers, and @action

A ViewSet groups related views for ONE resource into a single class, with methods named by ACTION (list/create/retrieve/update/partial_update/destroy) rather than by HTTP method — a genuinely different organizing principle from APIView's get()/post(). GenericViewSet combines that action-based structure with GenericAPIView's queryset/serializer_class machinery, but implements no actions itself; ModelViewSet adds every model mixin (list/create/retrieve/update/destroy) on top, becoming the ViewSet equivalent of combining ListCreateAPIView and RetrieveUpdateDestroyAPIView into ONE class. A Router (DefaultRouter, SimpleRouter) inspects a registered ViewSet's actions and auto-generates the matching URL patterns (list/create → /articles/, retrieve/update/destroy → /articles/<pk>/) — the one piece of DRF with no APIView/generic-view equivalent at all, since those still need urls.py written by hand. @action adds an extra, non-CRUD endpoint to a ViewSet (e.g. POST /articles/5/publish/), with detail=True/False controlling whether it's nested under a specific object's URL or the collection's.

Key features

  • ViewSet action names are list/create/retrieve/update/partial_update/destroy — not get/post/put/patch/delete; a Router maps HTTP methods to these action names according to a fixed, documented convention
  • GenericViewSet provides queryset/serializer_class/get_object()/etc. but implements ZERO actions on its own — it must be combined with mixins (like a generic view built from scratch) to do anything
  • ModelViewSet = GenericViewSet + all five model mixins — the single-class equivalent of ListCreateAPIView + RetrieveUpdateDestroyAPIView combined, for the common "full CRUD on one resource" case
  • router.register(prefix, viewset, basename) adds a ViewSet's auto-generated URLs under prefix — DefaultRouter additionally generates a browsable API root view listing every registered ViewSet, SimpleRouter does not
  • @action(detail=True, methods=["post"]) creates a URL nested under a specific object (/articles/5/publish/); detail=False creates one under the collection (/articles/recent/) — both still go through the same ViewSet's permission/authentication setup
  • A ViewSet not registered with a Router has no automatic URLs at all — it can still be used with .as_view({"get": "list"}) manually mapping HTTP methods to actions, which is how a ViewSet can be wired into a plain urls.py path() when a Router's conventions don't fit

ViewSet action → URL, as a Router maps it

ViewSet action → URL, as a Router maps it
HTTP method + URLViewSet action
GET /articles/list
POST /articles/create
GET /articles/<pk>/retrieve
PUT / PATCH /articles/<pk>/update / partial_update
DELETE /articles/<pk>/destroy
A Router maps HTTP method + URL to a ViewSet action
GET → list
GET → retrieve
POST → create
PUT/PATCH → update
DELETE → destroy
  • GET → list: collection URL (/articles/), read
  • GET → retrieve: detail URL (/articles/<pk>/), read
  • POST → create: collection URL (/articles/), write
  • PUT/PATCH → update: detail URL (/articles/<pk>/), write
  • DELETE → destroy: detail URL (/articles/<pk>/), write

Command

python
class MyViewSet(viewsets.ModelViewSet):
    queryset = Model.objects.all()
    serializer_class = MySerializer

    @action(detail=True, methods=["post"])
    def custom_action(self, request, pk=None):
        ...

A ViewSet organizes code by RESOURCE (one class, actions named list/create/retrieve/update/partial_update/destroy), not by URL — a genuine shift from generic views' per-URL organization. ModelViewSet = GenericViewSet + every model mixin. A Router auto-generates URLs from a registered ViewSet's actions by convention; basename must be explicit when queryset isn't a class attribute. @action adds a non-CRUD endpoint — always fetch the object via self.get_object(), never a raw queryset lookup, to keep authorization consistent.

3.get_queryset(), get_serializer_class/context(), and perform_*()

get_queryset() overrides the class-level queryset attribute for per-request logic (filtering to request.user, a URL kwarg) — every generic view/mixin method calls this instead of reading self.queryset directly. get_serializer_class() similarly overrides serializer_class for per-request logic (a different serializer for list vs. detail, or based on request.method) — get_serializer() (note: no "_class") is the method that actually INSTANTIATES it, calling get_serializer_class() and get_serializer_context() internally. get_serializer_context() extends the default context ({"request", "view", "format"}) with anything extra a serializer needs. perform_create()/perform_update()/perform_destroy() are the hooks CreateModelMixin/UpdateModelMixin/DestroyModelMixin call to actually save/delete — overriding one of these (rather than create()/update()/destroy() themselves) is the documented way to inject additional logic (setting a field from request.user, an audit log entry) around the save, without re-implementing response formatting.

Key features

  • get_queryset() is called by every mixin method (list(), retrieve(), update(), destroy()) that needs a queryset — overriding it centralizes per-request filtering in ONE place rather than repeating it per action
  • get_serializer_class() defaults to returning self.serializer_class — override it to switch serializers by self.action (on a ViewSet) or self.request.method, e.g. a lighter serializer for list than for retrieve
  • get_serializer() is what actually calls get_serializer_class() to get a class, then instantiates it with get_serializer_context() merged in — call THIS (not the serializer class directly) inside a custom action to stay consistent with the view's own configuration
  • get_serializer_context() by default returns {"request": self.request, "view": self, "format": self.format_kwarg} — override to add project-specific context, calling super() first to keep the defaults
  • perform_create(serializer) defaults to serializer.save() — override to pass extra kwargs (perform_create(serializer): serializer.save(owner=self.request.user)) or add a side effect (queuing a task) around the save
  • perform_update()/perform_destroy() follow the same pattern as perform_create() — CreateModelMixin/UpdateModelMixin/DestroyModelMixin call the perform_*() hook specifically so response-building logic (status code, serialized output) never needs to be touched just to add a side effect around the actual save/delete

The hook methods, and what calls each

The hook methods, and what calls each
HookCalled byOverridden for
get_queryset()list(), retrieve(), update(), destroy()per-request row filtering
get_serializer_class()get_serializer()per-action/method serializer choice
get_serializer_context()get_serializer()extra context beyond request/view/format
perform_create/update/destroy()create(), update(), destroy()a side effect or extra field around the save/delete
Which hook, called by what

get_queryset()

called by list(), retrieve(), update(), destroy() — per-request row filtering

get_serializer_class()

called by get_serializer() — per-action/method serializer choice

get_serializer_context()

called by get_serializer() — extra context beyond request/view/format

perform_create/update/destroy()

called by create()/update()/destroy() — a side effect or server-set field around the save

  1. get_queryset() — called by list(), retrieve(), update(), destroy() — per-request row filtering
  2. get_serializer_class() — called by get_serializer() — per-action/method serializer choice
  3. get_serializer_context() — called by get_serializer() — extra context beyond request/view/format
  4. perform_create/update/destroy() — called by create()/update()/destroy() — a side effect or server-set field around the save

Command

python
def get_queryset(self):
    return super().get_queryset().filter(...)

def perform_create(self, serializer):
    serializer.save(extra_field=...)

get_queryset()/get_serializer_class()/get_serializer_context() are the per-request override points generic views and ViewSets call instead of reading class attributes directly — always call self.get_serializer(), never a serializer class directly, to keep context/class-selection consistent. perform_create()/update()/destroy() are where a side effect or a server-controlled field (like owner=self.request.user) belongs — never accept that kind of field as writable serializer input.

Remember

  • APIView gives DRF plumbing with every handler hand-written; a mixin gives one reusable behavior; a generic view gives the full common combination pre-wired.
  • A ViewSet organizes code by resource (list/create/retrieve/update/partial_update/destroy actions), not by URL — a Router maps those actions to URLs by convention.
  • @action needs self.get_object() for authorization consistency; basename must be explicit on a Router registration when queryset isn't a class attribute.
  • get_queryset()/get_serializer_class()/get_serializer_context() are the per-request override points; perform_create()/update()/destroy() are where a server-controlled field or side effect belongs — never as writable serializer input.

Pro tip

  • Always call self.get_serializer(), never a serializer class directly, inside a custom action — it's what assembles get_serializer_context() and resolves get_serializer_class().
  • Customize a generic view or ViewSet via its narrowest hook, not by copy-pasting and modifying list()/create() — the hook methods exist so a small change doesn't require reimplementing the whole method.

Next up

  • DRF Authentication
  • DRF Permissions
  • DRF Filtering, Search, and Ordering

Sources

  • DRF Generic views / ViewSets — django-rest-framework.org/api-guide/generic-views/
  • Every claim checked against Django REST Framework 3.15 docs · checked 2026-08-24

Spotted a mistake on this page? Tell us.