skip to content

ViewSets & Actions

ViewSets group list, retrieve, create, update and destroy actions in one class, and SimpleRouter or DefaultRouter generate the URLs and route names. Interviewers ask about @action and basename.

part ofDjango REST Frameworkoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In Django REST Framework, how does a ViewSet differ from an APIView, and what do ModelViewSet and ReadOnlyModelViewSet provide?

level: juniorimportance: must knowfreq 72%

answer

  1. actions instead of handlers
  2. binding happens at as_view()
  3. a router writes the URLs
  4. read-only: two actions

basics

~20 s

A ViewSet defines actions such as list() and retrieve() instead of get() and post() handlers; as_view() or a router binds HTTP methods to them. ModelViewSet provides all six CRUD actions; ReadOnlyModelViewSet provides only list() and retrieve().

solid answer

~40 s

An `APIView` has one handler per HTTP method, so a resource usually needs a collection view and an item view, each wired in the URLconf. A ViewSet groups the resource's **actions** — `list()`, `create()`, `retrieve()`, `update()`, `partial_update()`, `destroy()` — in one class with no `get()`/`post()` of its own; the binding happens in `as_view({'get': 'list', ...})`, which a router calls for you when you `register()` the ViewSet. `ModelViewSet` gets all six actions from the generic mixins on top of `GenericViewSet`; `ReadOnlyModelViewSet` gets only `list()` and `retrieve()`. The tradeoff is visibility: URLs are implicit and `ModelViewSet` exposes delete and update unless you compose a narrower class.

code

python · 14 lines
python
from rest_framework import mixins, viewsets

from .models import Project
from .serializers import ProjectSerializer


class ProjectViewSet(mixins.ListModelMixin,
                     mixins.RetrieveModelMixin,
                     mixins.CreateModelMixin,
                     mixins.UpdateModelMixin,
                     viewsets.GenericViewSet):
    """Projects can be listed, read, created and edited, never deleted."""
    queryset = Project.objects.all()
    serializer_class = ProjectSerializer

go deeper

for a junior

Recall that a ViewSet holds actions, not handlers, and name the actions ModelViewSet and ReadOnlyModelViewSet provide.

for a middle

Explain how as_view() binds methods to actions, why it refuses to run without a mapping, and how GenericViewSet differs from ViewSet.

for a senior

Argue for the narrowest base class: compose GenericViewSet with chosen mixins so the API exposes only intended operations, and keep non-resource endpoints as plain views.

for a principal

Weigh router-generated consistency against explicit URLconfs for a team, and set a convention for when a resource earns a ViewSet.

## Handlers versus actions In Django REST Framework (DRF), an `APIView` is organised around **HTTP method handlers**: you write `get()`, `post()`, `put()` and so on, and Django's class-based view dispatch calls the one that matches the request. Each URL gets its own view class, so a typical resource needs two classes — one for the collection URL and one for the item URL. A **ViewSet** is organised around **actions** instead: `list()`, `create()`, `retrieve()`, `update()`, `partial_update()` and `destroy()`, plus any extra actions you add. A ViewSet class defines no `get()` or `post()` at all. The binding from HTTP method to action happens **when the view function is created**, not in the class: - `ProjectViewSet.as_view({"get": "list", "post": "create"})` produces the collection view; - `ProjectViewSet.as_view({"get": "retrieve", "put": "update", "patch": "partial_update", "delete": "destroy"})` produces the item view; - calling `as_view()` on a ViewSet **without** an actions mapping raises `TypeError`; - if `get` is mapped and `head` is not, DRF maps `head` to the same action. In practice you rarely write those calls. You **register** the ViewSet with a router, and the router generates both URL patterns, with the right mappings and route names, from one line. ## The four ViewSet classes | Class | Built from | Actions provided | |---|---|---| | `ViewSet` | `ViewSetMixin` + `APIView` | none — you write `list()`, `retrieve()` and so on by hand | | `GenericViewSet` | `ViewSetMixin` + `GenericAPIView` | none, but `get_queryset()`, `get_object()`, `get_serializer()`, pagination and filtering are available | | `ReadOnlyModelViewSet` | `RetrieveModelMixin` + `ListModelMixin` + `GenericViewSet` | `list()`, `retrieve()` | | `ModelViewSet` | the create, retrieve, update, destroy and list mixins + `GenericViewSet` | `list()`, `create()`, `retrieve()`, `update()`, `partial_update()`, `destroy()` | `ViewSetMixin` is the piece that makes a class a ViewSet: it overrides `as_view()` to accept the actions mapping, and it sets `self.action` on each request so your code can tell which action is running. ## A projects API in a few lines ```python from rest_framework import routers, viewsets from .models import Project from .serializers import ProjectSerializer class ProjectViewSet(viewsets.ModelViewSet): queryset = Project.objects.all() serializer_class = ProjectSerializer router = routers.DefaultRouter() router.register("projects", ProjectViewSet) urlpatterns = router.urls ``` This gives `GET`/`POST` on `projects/` and `GET`/`PUT`/`PATCH`/`DELETE` on `projects/<pk>/`, with route names `project-list` and `project-detail` derived from the model. ## What happens on a request For `GET projects/7/` routed to the item view, the sequence is: 1. The router-built view function instantiates `ProjectViewSet` and, for each entry in its mapping, sets the instance attribute `get` to the bound `retrieve` method — this is the "binding". 2. `initialize_request()` wraps the request and sets `self.action = "retrieve"`. 3. `initial()` runs authentication, permission and throttle checks as for any `APIView`. 4. Dispatch calls `self.get(...)`, which is now `retrieve()`, and the mixin builds the response. The class itself never changes; each request gets a fresh instance with its own bindings. ## When a ViewSet is the right tool - **Conventional resources**: several models exposed with the same CRUD shape, where consistency across endpoints matters more than per-endpoint control. - **Resource-specific operations** such as archiving a project, which fit naturally as extra actions on the same class. - **Consistent URLs and names**: every resource gets the same URL layout and the same `<basename>-list` / `<basename>-detail` naming, which hyperlinked serializers rely on. ## When it hides too much The convenience has costs that interviewers like to probe: 1. **Everything is exposed by default.** `ModelViewSet` routes `DELETE` and `PUT` whether or not the product needs them. If projects must never be deleted through the API, compose `GenericViewSet` with only the mixins you want (for example list, retrieve, create and update) rather than subclassing `ModelViewSet` and hoping nobody calls `destroy()`. 2. **URLs are implicit.** A reader cannot see the URL layout in the URLconf; it lives in the router's route table. New team members grep for a path and find nothing. 3. **One class, many policies.** Different actions often need different permissions or serializers, which pushes branching on `self.action` into `get_permissions()` and `get_serializer_class()`. 4. **Non-resource endpoints fit badly.** A login exchange, a report or a webhook receiver is not a collection of objects; forcing it into a ViewSet produces empty or meaningless actions. An `APIView` or a function view decorated with `@api_view` is clearer there. A reasonable rule: use ViewSets and a router for model-backed resources with standard CRUD, choose the narrowest base class that provides only the actions you mean to expose, and use plain views for everything else.

  • In DRF, what does GenericViewSet add over ViewSet if neither provides any actions?
    `GenericViewSet` is `ViewSetMixin` on top of `GenericAPIView` rather than `APIView`, so it carries `queryset`, `serializer_class`, `get_queryset()`, `get_object()`, `get_serializer()` and the pagination and filtering hooks. That is what lets the model mixins, or your own actions, work without reimplementing lookups. A plain `ViewSet` suits actions that are not backed by a model queryset.
  • How would you hand-wire a DRF ViewSet into a URLconf without a router?
    Call `as_view()` with an explicit mapping per URL: `ProjectViewSet.as_view({'get': 'list', 'post': 'create'})` for the collection path and a second mapping for the item path, each placed in a `path()` entry. This is what the router does internally; doing it by hand makes URLs visible but loses generated route names unless you add them yourself.

A ViewSet is a staff roster listing who does which job; no call reaches anyone until the switchboard — the router, or as_view() with an actions map — wires each incoming line, a URL plus an HTTP method, to a named person.

saying these in an interview costs you the question

  • A ViewSet needs get() and post() methods like any class-based view.
  • ReadOnlyModelViewSet includes partial_update() for PATCH requests.
  • Calling as_view() on a ViewSet maps GET to list() by default.
  • ViewSets cannot be used without a router.
  • Subclassing ModelViewSet is the only way to get CRUD actions in a ViewSet.
open as a page

In a Django REST Framework ModelViewSet, how do you add POST /projects/{pk}/archive/ with @action, and what do detail, methods and url_path control?

level: middleimportance: must knowfreq 58%

basics

~10 s

Decorate a ViewSet method with @action(detail=True, methods=['post']). detail picks object versus collection URLs, methods defaults to GET only, url_path defaults to the method name, and the route is named <basename>-<method-name-with-hyphens>.

open as a page

In a Django REST Framework ModelViewSet, how do you vary permissions or serializers per action using self.action, and where does that break?

level: middleimportance: should knowfreq 44%

basics

~10 s

Branch on self.action in get_permissions() or get_serializer_class(), or pass permission_classes to @action. It breaks when branches allowlist dangerous actions, forget partial_update, ignore None and metadata, or read self.action inside get_parsers() or get_authenticators().

open as a page

In Django REST Framework, what URLs and route names do SimpleRouter and DefaultRouter generate for a ViewSet, and when must you pass basename?

level: middleimportance: should knowfreq 47%

basics

~20 s

Routers generate prefix/ (name basename-list), prefix/<lookup>/ (basename-detail) and one route per @action (basename-url_name). DefaultRouter also adds an api-root view and format suffixes. Pass basename when the ViewSet has no queryset attribute to infer it from.

open as a page

Two Django REST Framework ModelViewSets over the same Project model are registered on one router without basename; what happens in DRF 3.18, and what other route collisions should you check?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Both ViewSets infer the basename project, and since DRF 3.15 register() raises ImproperlyConfigured for the duplicate. The check is per router and basename only, so cross-router names, url_name clashes and collection actions shadowing lookup values still need review.

open as a page