Skip to main content

🧩 ViewSets and Routers

You've built endpoints view by view and URL by URL. ViewSets and Routers are DRF's highest level of abstraction — they bundle a full set of CRUD actions into a single class and generate all the URLs automatically. This is how professional Django APIs stay consistent and tiny.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain how a ViewSet maps HTTP methods to actions (list, create, retrieve, update, destroy)
  • Build a complete CRUD API with ModelViewSet in a handful of lines
  • Add custom endpoints with the @action decorator
  • Register ViewSets with a DefaultRouter to auto-generate URLs
  • Tailor available actions with mixins and add filtering, searching, and ordering
  • Apply per-action permissions and configure pagination

Estimated Time: 40–50 minutes  •  Difficulty: Intermediate

Hands-on: Convert a multi-view API into ViewSets + a router, and add a custom action.

In This Lesson

Why ViewSets & Routers?

Across the last two lessons you wrote a serializer, then a BookList view, a BookDetail view, and two URL patterns — just for one model. Add authors, publishers, and reviews and that boilerplate multiplies. ViewSets collapse the related views into one class, and Routers generate the URLs for you.

💡 From remote controls to a smart-home hub. Function-based views are like a separate remote for every device — explicit but repetitive. Class-based views are a universal remote per room — tidier, but you still set up each room. A ViewSet is the central hub: "turn on the lights" works the same everywhere. A Router is the voice assistant that routes each command to the right device without you wiring it up.

Understanding ViewSets

A ViewSet is a single class that handles a resource's whole lifecycle. Instead of separate view functions, it exposes named actions, and DRF maps HTTP methods onto them:

HTTP MethodViewSet ActionURLPurpose
GETlist/books/List all books
POSTcreate/books/Create a book
GETretrieve/books/{id}/Get one book
PUTupdate/books/{id}/Replace a book
PATCHpartial_update/books/{id}/Edit a book
DELETEdestroy/books/{id}/Delete a book

The ViewSet family builds on the generic views you already met:

flowchart TD A[GenericAPIView] --> B[GenericViewSet] B --> C[ReadOnlyModelViewSet
list + retrieve] B --> D[ModelViewSet
full CRUD] D --> E[list] D --> F[create] D --> G[retrieve] D --> H[update] D --> I[partial_update] D --> J[destroy]

Your First ViewSet

Here is a full CRUD API for the Book model from earlier lessons — the entire thing:

# catalog/views.py
from rest_framework import viewsets
from .models import Book
from .serializers import BookSerializer

class BookViewSet(viewsets.ModelViewSet):
    """View and edit book instances — full CRUD, automatically."""
    queryset = Book.objects.all()
    serializer_class = BookSerializer

Those five lines provide list, retrieve, create, update, partial-update, and delete. If you only need reads, swap the base class:

class BookViewSet(viewsets.ReadOnlyModelViewSet):
    """Only list and retrieve — no create/update/delete."""
    queryset = Book.objects.all()
    serializer_class = BookSerializer

💡 Different serializers per action

A common pattern is a lean serializer for lists and a rich one for details. Override get_serializer_class():

class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.all()

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

Custom Actions

Real APIs need more than plain CRUD — "mark as bestseller", "get this book's reviews", "list all bestsellers". The @action decorator adds these as first-class endpoints on the ViewSet.

from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import Book
from .serializers import BookSerializer, ReviewSerializer

class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

    @action(detail=True, methods=["post"])
    def mark_bestseller(self, request, pk=None):
        book = self.get_object()
        book.is_bestseller = True
        book.save()
        return Response(self.get_serializer(book).data)

    @action(detail=True, methods=["get"])
    def reviews(self, request, pk=None):
        reviews = self.get_object().reviews.all()
        page = self.paginate_queryset(reviews)
        if page is not None:
            serializer = ReviewSerializer(page, many=True)
            return self.get_paginated_response(serializer.data)
        return Response(ReviewSerializer(reviews, many=True).data)

    @action(detail=False, methods=["get"])
    def bestsellers(self, request):
        qs = self.get_queryset().filter(is_bestseller=True)
        return Response(self.get_serializer(qs, many=True).data)

📖 detail decides the URL shape

detail=True acts on a single object → /books/1/mark_bestseller/ and /books/1/reviews/.

detail=False acts on the collection → /books/bestsellers/.

methods=[...] lists the HTTP verbs the action accepts.

Routers: Automatic URLs

Register a ViewSet with a router and every URL — including your custom actions — is generated for you. No more hand-writing path() entries.

# catalog/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views

router = DefaultRouter()
router.register(r"books", views.BookViewSet)

urlpatterns = [
    path("api/", include(router.urls)),
]

That single registration produces:

  • GET/POST /api/books/ — list & create
  • GET/PUT/PATCH/DELETE /api/books/{pk}/ — retrieve, update, delete
  • POST /api/books/{pk}/mark_bestseller/ — custom action
  • GET /api/books/{pk}/reviews/ — custom action
  • GET /api/books/bestsellers/ — custom action

✅ Two router types

SimpleRouter — the URL patterns, nothing extra.

DefaultRouter — the same, plus a browsable API root that lists and links every registered ViewSet. Great during development.

Many ViewSets, one router

router = DefaultRouter()
router.register(r"books", views.BookViewSet)
router.register(r"authors", views.AuthorViewSet)
router.register(r"publishers", views.PublisherViewSet)

Each registration gets a full, consistent set of RESTful URLs — the whole point of routers.

Fine Control with Mixins

ModelViewSet is really a bundle of mixins on top of GenericViewSet. When you want some but not all actions, compose the mixins yourself:

from rest_framework import viewsets, mixins
from .models import Book
from .serializers import BookSerializer

class BookViewSet(mixins.ListModelMixin,
                  mixins.RetrieveModelMixin,
                  mixins.CreateModelMixin,
                  viewsets.GenericViewSet):
    """Provides list, retrieve, and create — but no update or delete."""
    queryset = Book.objects.all()
    serializer_class = BookSerializer
MixinAction(s) it adds
ListModelMixinlist
RetrieveModelMixinretrieve
CreateModelMixincreate
UpdateModelMixinupdate, partial_update
DestroyModelMixindestroy

Filtering, Permissions & Pagination

Filtering, searching & ordering

Add filter backends and declare which fields clients may filter, search, and sort by:

from rest_framework import viewsets
from rest_framework.filters import SearchFilter, OrderingFilter
from django_filters.rest_framework import DjangoFilterBackend
from .models import Book
from .serializers import BookSerializer

class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

    filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
    filterset_fields = ["author", "is_bestseller"]        # ?author=1&is_bestseller=true
    search_fields = ["title", "author__name"]             # ?search=django
    ordering_fields = ["title", "published_date"]          # ?ordering=-published_date
    ordering = ["-published_date"]                         # default

Per-action permissions

Different actions often need different rules — anyone can read, but only signed-in users create and only admins delete. Override get_permissions():

from rest_framework import viewsets, permissions

class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

    def get_permissions(self):
        if self.action in ("list", "retrieve"):
            classes = [permissions.AllowAny]
        elif self.action == "create":
            classes = [permissions.IsAuthenticated]
        else:  # update, partial_update, destroy
            classes = [permissions.IsAdminUser]
        return [c() for c in classes]

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)  # attach the current user

Pagination

ViewSets honour the project-wide PAGE_SIZE from settings.py, and you can override per ViewSet:

from rest_framework.pagination import PageNumberPagination

class LargeResultsPagination(PageNumberPagination):
    page_size = 100
    page_size_query_param = "page_size"
    max_page_size = 1000

class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer
    pagination_class = LargeResultsPagination

⚠️ Always paginate list endpoints

An unpaginated list over a growing table will eventually try to serialize thousands of rows in one response — slow for the server and the client. Setting a project-wide DEFAULT_PAGINATION_CLASS protects every ViewSet by default.

Hands-on Exercise

🏋️ Refactor to ViewSets + Router

Objective: Take the multi-view Book API and collapse it into a ViewSet with a router and a custom action.

Instructions:

  1. Replace your BookList and BookDetail generic views with a single BookViewSet(viewsets.ModelViewSet).
  2. Register it with a DefaultRouter under api/ and delete the old hand-written path() entries.
  3. Add a @action(detail=False) called recent that returns books published in the current year.
  4. Enable searching on title and ordering on published_date.
  5. Open the browsable API root and confirm every URL (including /api/books/recent/) is generated.
💡 Hint

For the current year, use from django.utils import timezone then timezone.now().year, and filter with published_date__year=.... The recent action is collection-level, so it's detail=False and lives at /api/books/recent/.

✅ Sample solution
# views.py
from django.utils import timezone
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.filters import SearchFilter, OrderingFilter
from .models import Book
from .serializers import BookSerializer

class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer
    filter_backends = [SearchFilter, OrderingFilter]
    search_fields = ["title"]
    ordering_fields = ["published_date"]

    @action(detail=False, methods=["get"])
    def recent(self, request):
        year = timezone.now().year
        qs = self.get_queryset().filter(published_date__year=year)
        page = self.paginate_queryset(qs)
        if page is not None:
            return self.get_paginated_response(
                self.get_serializer(page, many=True).data)
        return Response(self.get_serializer(qs, many=True).data)

# urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views

router = DefaultRouter()
router.register(r"books", views.BookViewSet)

urlpatterns = [path("api/", include(router.urls))]

🎯 Quick Quiz

Question 1: Which base class gives you a full set of CRUD actions with the least code?

Question 2: You add @action(detail=False, methods=["get"]) named bestsellers to a BookViewSet registered under books. What URL does the router create?

Question 3: What is the primary job of a Router like DefaultRouter?

Summary & Quiz

🎉 Key Takeaways

  • ViewSets bundle related views into one class; ModelViewSet gives full CRUD in a few lines.
  • DRF maps HTTP methods to named actions (list, create, retrieve, update, partial_update, destroy).
  • The @action decorator adds custom endpoints — detail=True for one object, detail=False for the collection.
  • Routers auto-generate URLs; DefaultRouter also adds a browsable API root.
  • Mixins let you expose exactly the actions you want.
  • Filtering, searching, ordering, per-action permissions, and pagination all plug straight into a ViewSet.

📚 Further Reading

🚀 What's Next?

You now command the full DRF toolkit — serializers, views, ViewSets, and routers. Next up is the Weekend Project, where you'll pull everything in this module together into a complete Django + DRF application.

🎉 Module skills complete!

From a lone model to a full, filtered, paginated REST API. Now let's build something real with it.