skip to content

API Views & Routers

DRF's view layer runs from APIView and @api_view through generic classes and mixins to ViewSets that routers wire into URLs. Interviewers ask when a ViewSet is worth its indirection.

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

explore

questions

13

In Django REST Framework, what does GenericAPIView add on top of APIView, and how do the mixins combine into concrete views like ListCreateAPIView?

level: juniorimportance: must knowfreq 62%

answer

  1. queryset plus serializer plumbing
  2. helpers, but no handlers
  3. five action mixins
  4. concrete class binds get/post

basics

~10 s

GenericAPIView adds queryset, serializer_class, lookup_field and helpers like get_queryset(), get_serializer() and get_object(). Five mixins supply list(), create(), retrieve(), update() and destroy(); concrete classes such as ListCreateAPIView bind get() and post() to them.

solid answer

~30 s

`GenericAPIView` extends `APIView` with model plumbing: the `queryset` and `serializer_class` attributes, `lookup_field` (default `'pk'`), and helpers — `get_queryset()`, `get_serializer()`, `get_object()`, `filter_queryset()`, `paginate_queryset()`. It defines no HTTP handlers. The five mixins in `rest_framework.mixins` provide **actions** — `list()`, `create()`, `retrieve()`, `update()`/`partial_update()`, `destroy()` — written against those helpers. Concrete generics are just mixins plus `GenericAPIView` plus one-line handlers: `ListCreateAPIView` maps `get()` to `list()` and `post()` to `create()`. You customise them by overriding hooks such as `get_queryset()` or `perform_create()`, not the actions.

code

python · 30 lines
python
from rest_framework import generics, mixins

from .models import Ticket
from .serializers import TicketSerializer


class TicketDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Ticket.objects.all()
    serializer_class = TicketSerializer


# The same endpoint composed by hand from the building blocks
class TicketDetailByHand(mixins.RetrieveModelMixin,
                         mixins.UpdateModelMixin,
                         mixins.DestroyModelMixin,
                         generics.GenericAPIView):
    queryset = Ticket.objects.all()
    serializer_class = TicketSerializer

    def get(self, request, *args, **kwargs):
        return self.retrieve(request, *args, **kwargs)

    def put(self, request, *args, **kwargs):
        return self.update(request, *args, **kwargs)

    def patch(self, request, *args, **kwargs):
        return self.partial_update(request, *args, **kwargs)

    def delete(self, request, *args, **kwargs):
        return self.destroy(request, *args, **kwargs)

go deeper

for a junior

Name the layers: APIView, GenericAPIView, the five mixins, and the concrete classes. Be able to say which concrete class serves a collection URL and which serves an item URL.

for a middle

Explain that mixins provide actions, not handlers, and walk through what create() does: validate, call perform_create(), return 201 with an optional Location header.

for a senior

Show judgement about the layer: generics for plain model CRUD customised through hooks, APIView when the endpoint is a command or report and overriding would bury the generic flow.

for a principal

Frame generics as a team convention: consistent status codes, pagination and permission flow across endpoints, and a review rule for when an endpoint may leave the generic path.

## Three layers, one inheritance chain Django REST Framework (DRF) builds its class-based API views in layers. Each layer adds one kind of behaviour, and you pick the highest layer that still fits your endpoint. | Layer | Class or module | What it adds | |---|---|---| | Base view | `APIView` | Request parsing, authentication, permission and throttle checks, content negotiation, exception handling. You write `get()`, `post()` and friends yourself. | | Generic base | `GenericAPIView` | Model-oriented plumbing: `queryset`, `serializer_class`, lookups, filtering and pagination helpers. Still **no HTTP handlers**. | | Behaviour mixins | `rest_framework.mixins` | Five **action methods**: `list()`, `create()`, `retrieve()`, `update()`/`partial_update()`, `destroy()`. | | Concrete generics | `rest_framework.generics` | Ready classes such as `ListCreateAPIView` that bind HTTP handlers to those actions. | ## What GenericAPIView adds `GenericAPIView` subclasses `APIView` and contributes attributes and helper methods that every model-backed endpoint needs: - **`queryset`** and **`get_queryset()`** — the base set of objects. The method is what the mixins call; the attribute is its default. - **`serializer_class`**, **`get_serializer_class()`** and **`get_serializer()`** — which serializer to use, and a factory that builds it with a context holding `request`, `view` and `format`. - **`lookup_field`** (default `'pk'`) and **`lookup_url_kwarg`** — which model field and which URL keyword identify one object. - **`get_object()`** — fetches one object from the queryset, returns 404 when it is missing, and runs object-level permission checks. - **`filter_backends`** / **`filter_queryset()`** and **`pagination_class`** / **`paginate_queryset()`** — hooks for the filtering and pagination policies configured globally or per view. A bare `GenericAPIView` subclass serves no data: without `get()`, `post()` and friends, a GET, POST, PUT, PATCH or DELETE gets **405 Method Not Allowed** (only `OPTIONS`, which `APIView` itself implements, still answers). ## The five mixins Each mixin implements one **action** in terms of the helpers above, so it works for any model. | Mixin | Action method | Success response | Save/delete hook | |---|---|---|---| | `ListModelMixin` | `list()` | 200, paginated if a paginator is set | — | | `CreateModelMixin` | `create()` | 201, plus a `Location` header when the data carries a `url` key | `perform_create(serializer)` | | `RetrieveModelMixin` | `retrieve()` | 200, or 404 from `get_object()` | — | | `UpdateModelMixin` | `update()`, `partial_update()` | 200 | `perform_update(serializer)` | | `DestroyModelMixin` | `destroy()` | 204 with no body | `perform_destroy(instance)` | The mixins deliberately define **actions, not handlers**. `create()` does not know it will be called from `post()`. That separation lets the same mixins serve both the concrete generic views and DRF's viewsets, where a router rather than a method name decides which action runs. ## Concrete generics: the binding layer A concrete generic is a few lines: a mixin list, `GenericAPIView`, and one-line handlers. `ListCreateAPIView`, for example, is `ListModelMixin + CreateModelMixin + GenericAPIView` with `get()` returning `self.list(...)` and `post()` returning `self.create(...)`. The full set: 1. `ListAPIView` — `get()` to `list()`. 2. `CreateAPIView` — `post()` to `create()`. 3. `RetrieveAPIView` — `get()` to `retrieve()`. 4. `UpdateAPIView` — `put()` to `update()`, `patch()` to `partial_update()`. 5. `DestroyAPIView` — `delete()` to `destroy()`. 6. `ListCreateAPIView`, `RetrieveUpdateAPIView`, `RetrieveDestroyAPIView` and `RetrieveUpdateDestroyAPIView` — the common pairings. Collection endpoints (`/tickets/`) typically use `ListCreateAPIView`; item endpoints (`/tickets/<pk>/`) use `RetrieveUpdateDestroyAPIView`. Two classes and two URL patterns cover a full CRUD resource. ## One request traced end to end Following a `POST /tickets/` through a `ListCreateAPIView` shows how the layers cooperate: 1. Django resolves the URL and calls the view function that `as_view()` produced. 2. `APIView.dispatch()` wraps the request in DRF's `Request`, then `initial()` runs authentication, permission and throttle checks. 3. Dispatch finds the concrete class's `post()` handler, which simply returns `self.create(request, *args, **kwargs)`. 4. `CreateModelMixin.create()` asks `GenericAPIView` for a serializer through `get_serializer()`, validates it, calls `perform_create()`, and returns a 201 `Response`. 5. Any exception raised along the way — a validation error, a permission denial — is turned into an error response by the view's exception handler. Every layer contributes one slice, and none of them needs to know about the others' internals. ## Choosing the layer - Use a **concrete generic** when the endpoint is plain list/create/retrieve/update/delete over one model; customise through hooks (`get_queryset()`, `get_serializer_class()`, `perform_create()`). - Compose **mixins with `GenericAPIView`** yourself when you need an unusual pairing, and write the handler lines explicitly. - Drop to **`APIView`** when the endpoint is not model CRUD at all — a login exchange, a report, a bulk command. Forcing such an endpoint into generics usually means overriding so much that the generic machinery only obscures the code. The practical interview point is that almost all customisation of a generic view happens by overriding a small **hook** rather than the action method, because the action method carries validation, status codes and headers you would otherwise have to copy.

  • Why do DRF's mixins define list() and create() instead of get() and post()?
    Separating the action from the HTTP method lets the same mixin be bound in different ways. Concrete generics bind `get()` to `list()`, while DRF's viewsets let a router map methods to actions, so `ModelViewSet` reuses exactly the same five mixins. If the mixins defined `get()` directly, a view could not host both `list()` and `retrieve()` behind different routes.
  • What happens when a client sends DELETE to a DRF ListCreateAPIView endpoint?
    The view has no `delete()` handler, so `APIView.dispatch()` falls back to `http_method_not_allowed()` and the client gets 405 Method Not Allowed. Deleting single tickets belongs on an item endpoint, typically a `RetrieveUpdateDestroyAPIView` routed at `/tickets/<pk>/`.

saying these in an interview costs you the question

  • A bare GenericAPIView subclass answers GET and POST automatically.
  • The DRF mixins define get() and post() handlers directly.
  • CreateModelMixin returns 200 OK after saving a new object.
  • ListCreateAPIView also deletes objects when sent a DELETE request.
  • Pagination and filtering only work on viewsets, not on generic views.
open as a page

In Django REST Framework, what do APIView and the @api_view decorator add over a plain Django view, and how do they differ?

level: juniorimportance: must knowfreq 66%

basics

~20 s

Both give a view DRF's Request and Response, content negotiation, authentication, permission and throttle checks, and API exception handling. APIView is a class with get()/post() methods and policy attributes; @api_view(['GET']) wraps a function into an APIView subclass, with policy decorators.

open as a page

In Django REST Framework, how do request.data and request.query_params differ from Django's request.POST and request.GET?

level: juniorimportance: must knowfreq 58%

basics

~10 s

request.query_params is DRF's name for Django's request.GET. request.data is the body parsed by the view's parsers from its Content-Type, for POST, PUT and PATCH alike, JSON included; Django's request.POST holds only form-encoded POST data.

open as a page

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

level: juniorimportance: must knowfreq 72%

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().

open as a page

In a Django REST Framework ListCreateAPIView for support tickets, how do you set the requester from request.user, and why override perform_create()?

level: middleimportance: must knowfreq 64%

basics

~10 s

Override perform_create(self, serializer) and call serializer.save(requester=self.request.user). CreateModelMixin.create() still validates input, calls the hook, and returns 201, so only the save step changes and none of the response plumbing is copied.

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 Django REST Framework generic views, when do you override get_queryset() and get_serializer_class(), and why not read self.queryset directly?

level: middleimportance: should knowfreq 50%

basics

~20 s

Override get_queryset() when the rows depend on the request, and get_serializer_class() when the data shape does. self.queryset is a shared class attribute; evaluating it would cache results across requests, so DRF clones it with .all() and raises RuntimeError on direct evaluation.

open as a page

In Django REST Framework, in what order does APIView.initial() run content negotiation, authentication, permissions and throttles, and why does that order matter?

level: middleimportance: should knowfreq 48%

basics

~20 s

APIView.initial() negotiates the renderer, determines the API version, authenticates, checks permissions, then checks throttles; only afterwards is the handler looked up. So errors render in the negotiated format, bad credentials fail even on AllowAny views, and permission-rejected requests never reach throttles.

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

In Django REST Framework, what does GenericAPIView.get_object() do step by step, and what breaks when a view fetches the object itself instead?

level: seniorimportance: should knowfreq 38%

basics

~20 s

get_object() filters get_queryset(), reads the lookup value from the URL kwarg, fetches with a 404-raising lookup, then runs check_object_permissions(). A hand-written Model.objects.get() skips scoping, filter backends and object permissions, and turns a missing id into a 500.

open as a page

An unauthenticated client calling a Django REST Framework endpoint gets 403 instead of the expected 401; how does DRF choose between them, and how do you change it?

level: seniorimportance: should knowfreq 34%

basics

~20 s

DRF raises NotAuthenticated when authenticators exist but none succeeded, then returns 401 only if the first authentication class supplies a WWW-Authenticate header; otherwise it coerces the status to 403. SessionAuthentication, first by default, supplies none; put a header-based class first.

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