Introduction
Building robust and scalable APIs is one of the most common needs in modern web development. Django REST Framework (DRF) provides developers with powerful tools to simplify this process, enabling you to write clean, maintainable, and efficient API endpoints.
One of the most valuable components in DRF is the ViewSet, which works in conjunction with Routers. Together, they drastically reduce boilerplate code by automatically generating API views and URL routes for standard CRUD operations — all from a single class definition.
This post explores how to create API views using ViewSets and Routers, how they differ from traditional views, how routing works, and why they’re essential for developing well-structured RESTful APIs.
1. Understanding API Views in Django REST Framework
Before diving into ViewSets, it’s important to understand what an API view is in DRF.
In Django REST Framework, an API view is a class or function that handles HTTP requests (GET, POST, PUT, PATCH, DELETE) and returns HTTP responses, usually in JSON format.
There are three common ways to write API views in DRF:
- Function-Based Views (FBVs) using
@api_viewdecorator. - Class-Based Views (CBVs) using
APIView. - ViewSets, which combine multiple related views into one class.
The third option — ViewSets — provides a more concise and structured approach, especially when working with models that follow standard CRUD patterns.
2. The Challenge with Traditional API Views
Let’s consider a simple example without ViewSets. Suppose you have a Book model, and you want to expose API endpoints to list all books, create new ones, retrieve a single book, update, and delete.
Using traditional views, you might write something like this:
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .models import Book
from .serializers import BookSerializer
class BookListCreateView(APIView):
def get(self, request):
books = Book.objects.all()
serializer = BookSerializer(books, many=True)
return Response(serializer.data)
def post(self, request):
serializer = BookSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class BookDetailView(APIView):
def get(self, request, pk):
book = Book.objects.get(pk=pk)
serializer = BookSerializer(book)
return Response(serializer.data)
def put(self, request, pk):
book = Book.objects.get(pk=pk)
serializer = BookSerializer(book, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, pk):
book = Book.objects.get(pk=pk)
book.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
While this works perfectly fine, it involves a lot of repetitive code.
You need to explicitly define every action (get, post, put, delete), and then you also need to add URL patterns manually.
This is where ViewSets and Routers shine.
3. What Are ViewSets?
A ViewSet is a class in Django REST Framework that combines logic for multiple related views into one unified class. Instead of writing separate views for listing, creating, updating, and deleting, you can define them all within a single ViewSet.
DRF automatically maps HTTP methods (like GET, POST, PUT, DELETE) to the appropriate actions (list, create, retrieve, update, destroy) based on conventions.
This design makes your code more concise, organized, and maintainable.
4. Example: Creating a Book ViewSet
Let’s see how easy it is to create a full-featured API using a ViewSet.
Step 1: Define the Model
In models.py:
from django.db import models
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.CharField(max_length=100)
published_date = models.DateField()
def __str__(self):
return self.title
Step 2: Define the Serializer
In serializers.py:
from rest_framework import serializers
from .models import Book
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = ['id', 'title', 'author', 'published_date']
Step 3: Create the ViewSet
In views.py:
from rest_framework import viewsets
from .models import Book
from .serializers import BookSerializer
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
Here’s what’s happening:
- The
BookViewSetinherits fromModelViewSet, which provides built-in implementations for all CRUD operations. - The
querysetdefines which objects the view operates on. - The
serializer_classdefines how the data will be converted to and from JSON.
That’s all you need — no need to define methods like get, post, or delete manually.
5. ModelViewSet: The Complete Package
ModelViewSet is one of the most commonly used ViewSets in DRF.
It combines the functionality of several mixins that provide built-in actions:
ListModelMixin– Handles listing of objects (GET /books/)CreateModelMixin– Handles creation of new objects (POST /books/)RetrieveModelMixin– Handles retrieving a single object (GET /books/<id>/)UpdateModelMixin– Handles updating objects (PUT /books/<id>/)DestroyModelMixin– Handles deletion of objects (DELETE /books/<id>/)
All these mixins are included by default in ModelViewSet, saving you from manually writing CRUD logic.
6. What Are Routers?
Once you have defined a ViewSet, the next step is to expose it as API endpoints via URLs. Instead of manually defining URL patterns for each view, Django REST Framework provides Routers.
A Router automatically generates URL patterns for all standard actions defined in your ViewSet.
Example Using DefaultRouter
In urls.py:
from rest_framework.routers import DefaultRouter
from .views import BookViewSet
router = DefaultRouter()
router.register(r'books', BookViewSet)
urlpatterns = router.urls
That’s it.
The DefaultRouter automatically generates the following endpoints:
| Endpoint | HTTP Method | Description |
|---|---|---|
/books/ | GET | List all books |
/books/ | POST | Create a new book |
/books/<id>/ | GET | Retrieve a specific book |
/books/<id>/ | PUT | Update a specific book |
/books/<id>/ | DELETE | Delete a specific book |
You can now access your API endpoints instantly without manually defining URL routes.
7. How Routers Work Internally
Routers work by mapping standard viewset actions (list, create, retrieve, update, destroy) to corresponding HTTP methods and URL paths.
For example:
- The
listaction is mapped to a GET request on/books/. - The
createaction is mapped to a POST request on/books/. - The
retrieveaction is mapped to a GET request on/books/<id>/. - The
updateaction is mapped to a PUT request on/books/<id>/. - The
destroyaction is mapped to a DELETE request on/books/<id>/.
This internal mapping makes the API consistent and predictable, adhering to RESTful conventions.
8. Different Types of Routers
Django REST Framework provides two main types of routers:
- SimpleRouter – Generates only the basic routes (list, create, retrieve, update, destroy).
- DefaultRouter – Includes everything from SimpleRouter but also adds a default API root endpoint that lists all registered routes.
If you navigate to the root endpoint (e.g., /api/), you’ll see a list of all available resources when using DefaultRouter.
Example
from rest_framework.routers import SimpleRouter
from .views import BookViewSet
router = SimpleRouter()
router.register(r'books', BookViewSet)
urlpatterns = router.urls
The difference is that SimpleRouter won’t generate the default root listing page, while DefaultRouter will.
9. Customizing ViewSets
You’re not limited to the default CRUD behavior. You can add custom actions to your ViewSets using the @action decorator.
Example: Adding a Custom Endpoint
from rest_framework.decorators import action
from rest_framework.response import Response
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
@action(detail=False, methods=['get'])
def recent(self, request):
recent_books = Book.objects.order_by('-published_date')[:5]
serializer = self.get_serializer(recent_books, many=True)
return Response(serializer.data)
This creates a new endpoint:
/books/recent/
This endpoint lists the five most recently published books.
The detail=False argument means it’s a collection action (applies to all objects, not a specific instance).
If you set detail=True, it would apply to a single book, like /books/<id>/custom_action/.
10. Using Permissions with ViewSets
You can apply permissions and authentication to your ViewSets the same way you do for regular API views.
Example
from rest_framework.permissions import IsAuthenticated
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
permission_classes = [IsAuthenticated]
This ensures that only authenticated users can access any of the /books/ endpoints.
11. Filtering, Searching, and Ordering in ViewSets
ViewSets integrate perfectly with DRF’s filtering and search features.
Example
from rest_framework import filters
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
filter_backends = [filters.SearchFilter, filters.OrderingFilter]
search_fields = ['title', 'author']
ordering_fields = ['published_date']
Now you can perform searches like:
/books/?search=django
/books/?ordering=-published_date
This makes your API dynamic and user-friendly without any additional logic.
12. Pagination with ViewSets
Pagination is essential for large datasets, and it can be easily added globally or per ViewSet.
Example
from rest_framework.pagination import PageNumberPagination
class BookPagination(PageNumberPagination):
page_size = 5
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
pagination_class = BookPagination
Now /books/ will return paginated results with five records per page.
13. Nested Routers for Related Models
Sometimes you want to nest routes for related models — for example, listing all books by a specific author.
DRF supports this using nested routers from third-party packages like drf-nested-routers.
Example
from rest_framework_nested import routers
from .views import AuthorViewSet, BookViewSet
router = routers.DefaultRouter()
router.register(r'authors', AuthorViewSet)
books_router = routers.NestedDefaultRouter(router, r'authors', lookup='author')
books_router.register(r'books', BookViewSet, basename='author-books')
urlpatterns = router.urls + books_router.urls
This generates endpoints like:
/authors/
/authors/<id>/
/authors/<id>/books/
This is especially useful when you need to represent hierarchical relationships in your API.
14. Overriding Default Methods
You can override any default ViewSet method (list, create, retrieve, update, destroy) to customize its behavior.
Example
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
def list(self, request, *args, **kwargs):
print("Custom list logic here")
return super().list(request, *args, **kwargs)
This allows you to extend the base functionality while still leveraging the automatic routing provided by DRF.
15. Benefits of Using ViewSets and Routers
Using ViewSets and Routers provides several advantages:
- Less Code – Reduces boilerplate by automatically generating CRUD operations.
- Consistency – Enforces standard RESTful patterns across all endpoints.
- Maintainability – Centralizes logic in a single class for each resource.
- Scalability – Easy to extend with filters, pagination, and permissions.
- Readability – Keeps your API structure clean and organized.
With ViewSets, your API definitions are concise and predictable, leading to fewer bugs and easier collaboration.
16. Testing Your ViewSets
Testing ViewSets is identical to testing any other DRF view.
Example
from rest_framework.test import APITestCase
from django.urls import reverse
from .models import Book
class BookAPITests(APITestCase):
def test_create_book(self):
url = reverse('book-list')
data = {'title': 'New Book', 'author': 'John Doe', 'published_date': '2025-01-01'}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, 201)
self.assertEqual(Book.objects.count(), 1)
Here, book-list is the automatically generated route name for the /books/ endpoint, and DRF’s APITestCase makes it easy to simulate API requests.
17. ViewSet vs. APIView: Key Differences
| Feature | ViewSet | APIView |
|---|---|---|
| Boilerplate Code | Minimal | Requires manual setup |
| Routing | Automatic via Routers | Manual URL configuration |
| Best For | CRUD-style endpoints | Custom, non-standard logic |
| Common Class | ModelViewSet | APIView |
If your endpoint follows standard CRUD operations, use ViewSet.
If it involves complex logic or doesn’t fit REST patterns, use APIView.
18. Organizing Large APIs
As your project grows, you may have multiple ViewSets. The recommended structure is:
project/
├── app/
│ ├── models.py
│ ├── serializers.py
│ ├── views/
│ │ ├── __init__.py
│ │ ├── book_views.py
│ │ ├── author_views.py
│ ├── urls.py
This separation makes your project modular and easy to maintain.
19. Example: Complete Setup
Here’s what a complete DRF setup looks like using ViewSets and Routers:
models.py
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.CharField(max_length=100)
published_date = models.DateField()
serializers.py
from rest_framework import serializers
from .models import Book
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = ['id', 'title', 'author', 'published_date']
views.py
from rest_framework import viewsets
from .models import Book
from .serializers import BookSerializer
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
urls.py
from rest_framework.routers import DefaultRouter
from .views import BookViewSet
router = DefaultRouter()
router.register(r'books', BookViewSet)
urlpatterns = router.urls
That’s an entire REST API — fully functional, RESTful, and ready to use — with just a few lines of code.
Leave a Reply