🧩 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
ModelViewSetin a handful of lines - Add custom endpoints with the
@actiondecorator - Register ViewSets with a
DefaultRouterto 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 Method | ViewSet Action | URL | Purpose |
|---|---|---|---|
| GET | list | /books/ | List all books |
| POST | create | /books/ | Create a book |
| GET | retrieve | /books/{id}/ | Get one book |
| PUT | update | /books/{id}/ | Replace a book |
| PATCH | partial_update | /books/{id}/ | Edit a book |
| DELETE | destroy | /books/{id}/ | Delete a book |
The ViewSet family builds on the generic views you already met:
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 & createGET/PUT/PATCH/DELETE /api/books/{pk}/— retrieve, update, deletePOST /api/books/{pk}/mark_bestseller/— custom actionGET /api/books/{pk}/reviews/— custom actionGET /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
| Mixin | Action(s) it adds |
|---|---|
ListModelMixin | list |
RetrieveModelMixin | retrieve |
CreateModelMixin | create |
UpdateModelMixin | update, partial_update |
DestroyModelMixin | destroy |
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:
- Replace your
BookListandBookDetailgeneric views with a singleBookViewSet(viewsets.ModelViewSet). - Register it with a
DefaultRouterunderapi/and delete the old hand-writtenpath()entries. - Add a
@action(detail=False)calledrecentthat returns books published in the current year. - Enable searching on
titleand ordering onpublished_date. - 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;
ModelViewSetgives full CRUD in a few lines. - DRF maps HTTP methods to named actions (list, create, retrieve, update, partial_update, destroy).
- The
@actiondecorator adds custom endpoints —detail=Truefor one object,detail=Falsefor the collection. - Routers auto-generate URLs;
DefaultRouteralso 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.