Django REST Framework Views: APIView vs ViewSets
DRF gives you a whole hierarchy of view classes, and understanding where each one sits in that hierarchy — and when to reach for which — is one of the most important skills in DRF development.
graph TD
A[View - Django's base] --> B[APIView - DRF's base view]
B --> C[GenericAPIView - adds queryset/serializer]
C --> D[ListAPIView]
C --> E[CreateAPIView]
C --> F[RetrieveAPIView]
C --> G[UpdateAPIView]
C --> H[DestroyAPIView]
C --> I[ListCreateAPIView]
C --> J[RetrieveUpdateDestroyAPIView]
B --> K[ViewSetMixin + GenericAPIView = GenericViewSet]
K --> L[ReadOnlyModelViewSet]
K --> M[ModelViewSet]
APIView: The Foundation
APIView is DRF's base view class. It's like Django's View class but with DRF superpowers — authentication checking, permission checking, content negotiation, and response rendering are all handled automatically.
Think of APIView as the foundation of a building. You wouldn't live in just a foundation — you'd build rooms on top of it. But everything above rests on it, and understanding how it works explains why the floors above behave the way they do.
How APIView Processes Requests
When a request arrives at an APIView, DRF runs through a predictable sequence of steps before your handler method is ever called:
- Authentication — DRF checks the
Authorizationheader, cookies, or other configured auth methods and populatesrequest.userandrequest.auth - Permission checks — DRF calls
has_permission()on each permission class. If any returnsFalse, the request is rejected immediately - Content negotiation — DRF inspects the
Acceptheader to determine which renderer should format the response - Your method handler — Your
get(),post(),put(), etc. method is invoked with the fully-prepared request object - Response rendering — DRF serializes your response data and applies the negotiated content type
APIView in Action
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticatedOrReadOnly
class ArticleListView(APIView):
permission_classes = [IsAuthenticatedOrReadOnly]
def get(self, request):
"""List all articles. Everyone can read."""
articles = Article.objects.all()
serializer = ArticleSerializer(articles, many=True, context={'request': request})
return Response(serializer.data)
def post(self, request):
"""Create a new article. Only authenticated users."""
serializer = ArticleSerializer(data=request.data, context={'request': request})
if serializer.is_valid():
serializer.save(author=request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
When to Use APIView
Use APIView when:
- You're building an endpoint that doesn't map cleanly to CRUD operations (e.g., a login endpoint, a password reset handler, a webhook receiver)
- You need very fine-grained control over the logic that generic views would hide
- The endpoint doesn't have a clear model/queryset backing it, or has complex custom business logic
- You're implementing a non-standard API pattern (e.g., batch operations, data imports)
GenericAPIView and Mixins
GenericAPIView extends APIView with attributes and methods for working with querysets and serializers. By itself, it provides:
- A
querysetattribute to specify which objects you're operating on - A
serializer_classattribute to specify which serializer handles serialization/deserialization - Convenience methods like
get_queryset()andget_serializer()
The real power comes from combining GenericAPIView with mixins. Each mixin provides exactly one method that implements a single action:
ListModelMixinprovideslist()for GET collection requestsCreateModelMixinprovidescreate()for POST requestsRetrieveModelMixinprovidesretrieve()for GET single-object requestsUpdateModelMixinprovidesupdate()for PUT requestsDestroyModelMixinprovidesdestroy()for DELETE requests
DRF provides pre-built combinations like ListCreateAPIView and RetrieveUpdateDestroyAPIView that save you from manually composing mixins.
Generic View Example
from rest_framework import generics
from rest_framework.permissions import IsAuthenticatedOrReadOnly
class ArticleListCreateView(generics.ListCreateAPIView):
queryset = Article.objects.select_related('author').all()
serializer_class = ArticleSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
def perform_create(self, serializer):
serializer.save(author=self.request.user)
This single view class handles both GET /articles/ (list) and POST /articles/ (create), and you only override perform_create() to add custom save logic.
The perform_* Hook Pattern
The perform_create() hook is a beautiful DRF pattern. Instead of overriding the entire post() method and duplicating all the validation logic, you just override the moment where serializer.save() is called:
def perform_create(self, serializer):
"""Called after the serializer is valid, before save() is called."""
serializer.save(
author=self.request.user,
ip_address=get_client_ip(self.request),
)
There are similar hooks: perform_update(serializer) and perform_destroy(obj). These keep the validation/deserialization logic (which DRF handles) separate from your business logic (which you control).
ViewSets: The Power Move
A ViewSet groups related actions together into a single class. ModelViewSet is the most powerful built-in ViewSet:
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticatedOrReadOnly
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.select_related('author').prefetch_related('tags').all()
serializer_class = ArticleSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
def get_queryset(self):
"""Override to filter based on query params or current user."""
qs = super().get_queryset()
if self.action == 'list':
qs = qs.filter(status='published')
return qs
def perform_create(self, serializer):
serializer.save(author=self.request.user)
Notice self.action — it tells you which operation is being performed (list, create, retrieve, update, partial_update, destroy). You can conditionally apply different logic per action.
Routing a ViewSet
from rest_framework.routers import DefaultRouter
from django.urls import path, include
router = DefaultRouter()
router.register('articles', ArticleViewSet, basename='article')
urlpatterns = [
path('api/', include(router.urls)),
]
The router generates all six standard endpoints automatically from a single registration.
Custom Actions with @action
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import status
from django.utils import timezone
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
@action(detail=True, methods=['post'])
def publish(self, request, pk=None):
"""POST /articles/{id}/publish/ — publish a draft article."""
article = self.get_object() # Always use get_object() — it runs permission checks!
if article.status != 'draft':
return Response(
{'error': 'Only draft articles can be published.'},
status=status.HTTP_400_BAD_REQUEST
)
article.status = 'published'
article.published_at = timezone.now()
article.save()
return Response({'status': 'Article published.'})
@action(detail=False, methods=['get'])
def featured(self, request):
"""GET /articles/featured/ — list featured articles."""
featured = Article.objects.filter(is_featured=True)
serializer = self.get_serializer(featured, many=True)
return Response(serializer.data)
The get_object() Security Principle
A critical security principle: in custom actions, always retrieve objects using self.get_object(), not self.queryset.get(pk=pk) or Article.objects.get(pk=pk).
Why? Because get_object() automatically calls check_object_permissions(), which runs has_object_permission() on all your permission classes. If you bypass get_object(), you bypass your entire object-level security layer.
When to Use What: A Decision Guide
graph TD
A["Is this endpoint CRUD on a model?"] --> |Yes| B["Use ModelViewSet"]
A --> |No| C{"Is it custom logic for<br/>a model object?"}
C --> |Yes| D["Use APIView"]
C --> |No| E["Use APIView"]
B --> F["Apply @action decorator<br/>for custom endpoints"]
G["Read-only access only?"] --> |Yes| H["Use ReadOnlyModelViewSet"]
G --> |No| B
I["Only allow specific<br/>CRUD operations?"] --> |Yes| J["Use GenericAPIView<br/>+ custom mixins"]
I --> |No| K["Use ModelViewSet<br/>or ListCreateAPIView, etc."]
ModelViewSet: Standard CRUD on a model. Your default choice for 80% of your endpoints.ReadOnlyModelViewSet: Onlylist()andretrieve()allowed. Perfect for reference data that users can read but not modify.GenericAPIView+ mixins: When you want CRUD-like behavior with specific action combinations.APIView: Non-CRUD endpoints. Login, logout, password reset, webhook receivers, batch operations.
Only visible to you
Sign in to take notes.