ViewSets & Routers
Collapse a full CRUD API into one class with ModelViewSet, wire it up with routers, and customise with actions, get_queryset and get_serializer_class.
A ViewSet groups the logic for a resource — list, retrieve, create, update, destroy — into a single class, and a router generates the URL patterns for it. Together they turn a whole CRUD endpoint into a dozen lines.
The workhorse
ModelViewSet gives you all five CRUD operations. You mostly customise three hooks:
get_queryset()— scope rows to the request (per-user, per-tenant) and attach yourselect_related/prefetch_relatedhere.get_serializer_class()— return a lightweight serializer forlistand a detailed one forretrieve, or a different write serializer.perform_create()— inject server-side data likeserializer.save(owner=self.request.user).
Custom endpoints with @action
Not everything is CRUD. The @action decorator adds extra routes onto the same viewset — POST /orders/{id}/cancel/ — with detail=True for single-object actions or detail=False for collection actions.
Routers
Register the viewset with a DefaultRouter and include router.urls. The router names the routes consistently (order-list, order-detail) so reverse() and DRF's hyperlinked fields just work.
Example
from rest_framework import viewsets, permissions, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.routers import DefaultRouter
from .models import Order
from .serializers import OrderSerializer, OrderListSerializer
class OrderViewSet(viewsets.ModelViewSet):
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
# Scope to the user AND kill N+1 in one place for every action.
return (
Order.objects
.filter(owner=self.request.user)
.prefetch_related("items__product")
)
def get_serializer_class(self):
# Slim serializer for the list, full one for detail/write.
return OrderListSerializer if self.action == "list" else OrderSerializer
def perform_create(self, serializer):
serializer.save(owner=self.request.user) # never trust client for owner
@action(detail=True, methods=["post"])
def cancel(self, request, pk=None): # POST /orders/{id}/cancel/
order = self.get_object()
if order.status == "shipped":
return Response({"detail": "Already shipped."},
status=status.HTTP_409_CONFLICT)
order.status = "cancelled"
order.save(update_fields=["status"])
return Response(self.get_serializer(order).data)
# urls.py
router = DefaultRouter()
router.register(r"orders", OrderViewSet, basename="order")
urlpatterns = router.urls # generates list/detail/cancel routes automaticallyWhen to use it
- A developer uses a ReadOnlyModelViewSet to expose a public API that allows browsing but not modification.
- A team overrides perform_update() in a ModelViewSet to add an audit log entry every time a record is changed.
- A developer uses get_permissions() in a ViewSet to apply different permission classes for list vs detail actions.
More examples
Action-specific permissions
Returns AllowAny for read actions and IsAuthenticated for write actions on the same ViewSet.
from rest_framework.permissions import IsAuthenticated, AllowAny
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
def get_permissions(self):
if self.action in ("list", "retrieve"):
return [AllowAny()]
return [IsAuthenticated()]Override perform_create for audit log
Saves the new post and creates an audit log entry in the same perform_create() override.
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
def perform_create(self, serializer):
post = serializer.save(author=self.request.user)
AuditLog.objects.create(
user=self.request.user,
action="create",
object_id=post.pk,
)ReadOnlyModelViewSet for public API
Provides a read-only public API for published posts with no authentication required.
from rest_framework import viewsets
from rest_framework.permissions import AllowAny
class PublicPostViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Post.objects.filter(draft=False)
serializer_class = PostSerializer
permission_classes = [AllowAny]
Discussion