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()andget_serializer()helpersget_object()(useslookup_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
APIViewover aModelViewSet?” — endpoints that aren’t a resource: webhooks, login/logout, password reset, RPC actions, ad-hoc reports. - “
ModelViewSetvsGenericViewSet+ mixins — pick one.” —ModelViewSetwhen 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 attributequerysetruns once at import. - “Difference between
create()andperform_create()?” —create()does serializer validation + response;perform_create()is the save hook. Overrideperform_createfor “setowner=request.user” — keeps the validation/response code untouched. - “How does the router know which methods on a
ViewSetto wire up?” — by action name (list,create,retrieve, …) for default actions, and by@actiondecorator for custom ones. See 05_routers_actions.md.