backend / web frameworks / django / drf / 04_views_apiview_viewsets.md

DRF Views: APIView, GenericAPIView, ViewSet

5 interview angles 4 min read source

DRF Views: APIView, GenericAPIView, ViewSet

DRF gives you a ladder of view classes. Each rung trades flexibility for boilerplate.

APIView          ← raw, no queryset/serializer awareness
  └─ GenericAPIView   ← knows about queryset + serializer_class
        └─ generics.ListAPIView, CreateAPIView, RetrieveUpdateDestroyAPIView, ...

                ViewSet           ← groups action methods on one class
                  └─ GenericViewSet (+ mixins)
                        └─ ModelViewSet  ← all CRUD in one class

Pick the lowest rung that meets your needs. Don’t subclass APIView to reimplement what ModelViewSet already does.

APIView — the base

Use when the endpoint doesn’t fit the CRUD model (webhook receiver, RPC-style action, custom auth flow).

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

class WebhookView(APIView):
    authentication_classes = []
    permission_classes = []

    def post(self, request):
        verify_signature(request)
        process_event(request.data)
        return Response(status=204)

You write get / post / put / etc. directly. No queryset, no serializer_class, no auto pagination.

GenericAPIView — adds queryset + serializer_class

Rarely used directly. It’s the parent of the generics.* shortcuts and gives you:

  • get_queryset() and get_serializer() helpers
  • get_object() (uses lookup_field + lookup_url_kwarg)
  • paginate_queryset(), filter_queryset()

generics.* — concrete CRUD classes

from rest_framework import generics

class BookList(generics.ListCreateAPIView):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

class BookDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

Available combinations:

Class Methods
ListAPIView GET (list)
CreateAPIView POST
ListCreateAPIView GET, POST
RetrieveAPIView GET (one)
UpdateAPIView PUT, PATCH
DestroyAPIView DELETE
RetrieveUpdateDestroyAPIView GET, PUT, PATCH, DELETE

Use these when each URL maps to a single class and you don’t want auto-routing.

ViewSet — group actions on one class, route them automatically

from rest_framework import viewsets

class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.select_related("author")
    serializer_class = BookSerializer

Combined with a router this gives you 6 URLs from one class. Methods you can override:

Action When called
list(self, request) GET on the collection
create(self, request) POST on the collection
retrieve(self, request, pk=None) GET on the detail
update(self, request, pk=None) PUT
partial_update(self, request, pk=None) PATCH
destroy(self, request, pk=None) DELETE

For business logic, override the inner hooks instead — they keep validation/serialization free:

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

def perform_update(self, serializer):
    serializer.save(updated_by=self.request.user)

def perform_destroy(self, instance):
    instance.is_deleted = True   # soft delete
    instance.save()

ModelViewSet vs ReadOnlyModelViewSet vs GenericViewSet+Mixins

Class Endpoints
ModelViewSet list, create, retrieve, update, partial_update, destroy
ReadOnlyModelViewSet list, retrieve only
GenericViewSet + mixins.ListModelMixin + mixins.CreateModelMixin exactly the actions you mix in

Use GenericViewSet + mixins when you want, say, list + create but not delete.

Filtering the queryset per-request

Override get_queryset() — it’s the canonical place for “current user only” or “tenant scoping.”

def get_queryset(self):
    return Book.objects.filter(owner=self.request.user)

Don’t filter on the class-level queryset attribute (it’s evaluated once at import time and ignores the request).

Different serializer per action

def get_serializer_class(self):
    if self.action == "list":
        return BookListSerializer
    if self.action == "create":
        return BookCreateSerializer
    return BookDetailSerializer

Different permissions per action

def get_permissions(self):
    if self.action in ("list", "retrieve"):
        return [permissions.AllowAny()]
    return [permissions.IsAuthenticated()]

See 06_permissions.md.

When NOT to use a ViewSet

  • The endpoint is genuinely non-CRUD (webhook, search, login, async job submit).
  • The URL doesn’t fit /resource/ and /resource/{pk}/.
  • You have one class doing five unrelated things — split into multiple APIViews.

ViewSets are for resources. RPC-flavored endpoints belong on APIView.

Function-based views: @api_view

Still supported, mostly for very small endpoints or quick prototypes.

from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated

@api_view(["GET"])
@permission_classes([IsAuthenticated])
def health(request):
    return Response({"status": "ok"})

You lose get_serializer_class, get_queryset, mixins, browsable API metadata. Fine for /health and webhooks.

Interview angle

  • “When would you use APIView over a ModelViewSet?” — endpoints that aren’t a resource: webhooks, login/logout, password reset, RPC actions, ad-hoc reports.
  • ModelViewSet vs GenericViewSet + mixins — pick one.”ModelViewSet when you need all 6 CRUD ops; mixins when you want a subset (e.g. list + retrieve + create but no destroy).
  • “Where do you put per-user queryset scoping?”get_queryset(self). The class attribute queryset runs once at import.
  • “Difference between create() and perform_create()?”create() does serializer validation + response; perform_create() is the save hook. Override perform_create for “set owner=request.user” — keeps the validation/response code untouched.
  • “How does the router know which methods on a ViewSet to wire up?” — by action name (list, create, retrieve, …) for default actions, and by @action decorator for custom ones. See 05_routers_actions.md.