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 your select_related/prefetch_related here.
  • get_serializer_class() — return a lightweight serializer for list and a detailed one for retrieve, or a different write serializer.
  • perform_create() — inject server-side data like serializer.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

Example · python
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 automatically

When 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.

Example · python
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.

Example · python
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.

Example · python
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

  • Be the first to comment on this lesson.