Django REST ViewSets
Introduction
When building APIs with Django REST Framework (DRF), you'll often find yourself creating multiple views that perform related operations on a single resource. For instance, you might need views to list all resources, retrieve a single resource, create, update, and delete resources.
ViewSets in Django REST Framework provide an abstraction that lets you combine the logic for a set of related views into a single class. Instead of defining separate classes for actions like GET
, POST
, PUT
, and DELETE
operations, ViewSets allow you to group these related operations together.
In this tutorial, we'll explore:
- What are ViewSets and why they are useful
- Different types of ViewSets
- How to implement ViewSets
- Customizing ViewSet behavior
- Using Routers with ViewSets
What are ViewSets?
ViewSets are classes that combine the logic for multiple related views into a single class. They replace the need for multiple distinct views that are closely related. Instead of defining separate view classes for list, create, retrieve, update, and delete operations, a single ViewSet class can handle all these actions.
Here's a simple comparison:
Without ViewSets:
class BookListView(APIView):
def get(self, request):
# List all books
class BookDetailView(APIView):
def get(self, request, pk):
# Get a single book
def put(self, request, pk):
# Update a book
def delete(self, request, pk):
# Delete a book
With ViewSets:
class BookViewSet(viewsets.ViewSet):
def list(self, request):
# List all books
def retrieve(self, request, pk=None):
# Get a single book
def update(self, request, pk=None):
# Update a book
def destroy(self, request, pk=None):
# Delete a book
The ViewSet approach reduces code duplication and makes your API more consistent.
Types of ViewSets
Django REST Framework provides several types of ViewSets:
1. ViewSet
The base ViewSet class provides actions like list()
, create()
, retrieve()
, update()
, partial_update()
, and destroy()
. You need to implement these methods yourself.
2. GenericViewSet
Inherits from GenericAPIView
and provides default implementations for the core view behavior, but doesn't include any actions by default.
3. ReadOnlyModelViewSet
Includes actions for read-only operations: list()
and retrieve()
.
4. ModelViewSet
The most complete ViewSet that provides implementations for all CRUD operations: list()
, create()
, retrieve()
, update()
, partial_update()
, and destroy()
.
Creating Your First ViewSet
Let's start by creating a simple model:
# models.py
from django.db import models
class Book(models.Model):
title = models.CharField(max_length=100)
author = models.CharField(max_length=100)
publication_date = models.DateField()
def __str__(self):
return self.title
Next, let's create a serializer for our Book model:
# serializers.py
from rest_framework import serializers
from .models import Book
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = ['id', 'title', 'author', 'publication_date']
Now, let's create a ViewSet for our Book model:
# 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
With just a few lines of code, we've created a fully functional API that supports:
- Listing all books (
GET /books/
) - Creating a new book (
POST /books/
) - Retrieving a single book (
GET /books/{id}/
) - Updating a book (
PUT /books/{id}/
) - Partially updating a book (
PATCH /books/{id}/
) - Deleting a book (
DELETE /books/{id}/
)
Setting Up URLs with Routers
ViewSets are designed to work with Router classes, which automatically generate URL patterns for your API.
# urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import BookViewSet
router = DefaultRouter()
router.register(r'books', BookViewSet)
urlpatterns = [
path('api/', include(router.urls)),
]
This router will generate URLs for all the standard actions:
/api/books/
- List and create books/api/books/{pk}/
- Retrieve, update, or delete a specific book
Customizing ViewSet Actions
You can customize the behavior of your ViewSet by overriding the methods or adding new actions.
Overriding Default Methods
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
def list(self, request):
# Custom list implementation
queryset = self.filter_queryset(self.get_queryset())
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
Adding Custom Actions
You can add custom actions to a ViewSet using the @action
decorator:
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=True, methods=['post'])
def mark_as_read(self, request, pk=None):
book = self.get_object()
# Logic to mark book as read
return Response({'status': 'book marked as read'})
@action(detail=False, methods=['get'])
def newest(self, request):
newest_book = Book.objects.order_by('-publication_date').first()
serializer = self.get_serializer(newest_book)
return Response(serializer.data)
In this example:
mark_as_read
is a custom action that applies to a single book (detail=True), accessible viaPOST /api/books/{pk}/mark_as_read/
newest
is a custom action that applies to the collection (detail=False), accessible viaGET /api/books/newest/
Permissions and Authentication
You can add permissions to your ViewSet just like you would with a regular view:
from rest_framework import permissions
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
permission_classes = [permissions.IsAuthenticated]
You can also set different permissions for different actions:
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
def get_permissions(self):
if self.action in ['list', 'retrieve']:
permission_classes = [permissions.AllowAny]
else:
permission_classes = [permissions.IsAuthenticated]
return [permission() for permission in permission_classes]
Filtering and Pagination
ViewSets work seamlessly with DRF's filtering and pagination features:
from rest_framework import filters
from django_filters.rest_framework import DjangoFilterBackend
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['author'] # Allow filtering by author
search_fields = ['title', 'author'] # Allow searching in title and author
ordering_fields = ['publication_date'] # Allow ordering by publication_date
Real-World Example
Let's look at a more complete example that includes multiple related models:
# models.py
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=100)
bio = models.TextField()
class Book(models.Model):
title = models.CharField(max_length=100)
author = models.ForeignKey(Author, related_name='books', on_delete=models.CASCADE)
publication_date = models.DateField()
isbn = models.CharField(max_length=13)
def __str__(self):
return self.title
class Review(models.Model):
book = models.ForeignKey(Book, related_name='reviews', on_delete=models.CASCADE)
user_name = models.CharField(max_length=100)
rating = models.IntegerField(choices=[(i, i) for i in range(1, 6)])
comment = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
# serializers.py
from rest_framework import serializers
from .models import Author, Book, Review
class ReviewSerializer(serializers.ModelSerializer):
class Meta:
model = Review
fields = ['id', 'user_name', 'rating', 'comment', 'created_at']
class BookSerializer(serializers.ModelSerializer):
reviews = ReviewSerializer(many=True, read_only=True)
class Meta:
model = Book
fields = ['id', 'title', 'author', 'publication_date', 'isbn', 'reviews']
class AuthorSerializer(serializers.ModelSerializer):
books = BookSerializer(many=True, read_only=True)
class Meta:
model = Author
fields = ['id', 'name', 'bio', 'books']
# views.py
from rest_framework import viewsets, permissions
from .models import Author, Book, Review
from .serializers import AuthorSerializer, BookSerializer, ReviewSerializer
class AuthorViewSet(viewsets.ModelViewSet):
queryset = Author.objects.all()
serializer_class = AuthorSerializer
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
@action(detail=True, methods=['get'])
def reviews(self, request, pk=None):
book = self.get_object()
reviews = book.reviews.all()
serializer = ReviewSerializer(reviews, many=True)
return Response(serializer.data)
class ReviewViewSet(viewsets.ModelViewSet):
queryset = Review.objects.all()
serializer_class = ReviewSerializer
def create(self, request):
# Add custom logic for review creation
# For example, validation or sending notifications
return super().create(request)
# urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import AuthorViewSet, BookViewSet, ReviewViewSet
router = DefaultRouter()
router.register(r'authors', AuthorViewSet)
router.register(r'books', BookViewSet)
router.register(r'reviews', ReviewViewSet)
urlpatterns = [
path('api/', include(router.urls)),
]
This setup creates a complete bookstore API with related models and nested resources.
Summary
ViewSets in Django REST Framework provide a powerful way to organize related view logic into a single class. By using ViewSets, you can:
- Reduce code duplication
- Create consistent API interfaces
- Simplify URL configuration with routers
- Easily add custom actions to resources
- Apply permissions and authentication in a centralized way
- Seamlessly integrate with DRF's filtering and pagination features
ViewSets are particularly useful when building CRUD-based APIs where you have standard operations that apply to a resource. They help enforce consistency across your API and make your codebase more maintainable.
Additional Resources
Exercises
- Create a simple blog API with a
Post
model and aPostViewSet
that supports CRUD operations. - Add a custom action to your
PostViewSet
to mark a post as featured. - Implement a
CommentViewSet
that relates to yourPost
model. - Add filtering and search capabilities to your ViewSets.
- Implement custom permissions that only allow post authors to update or delete their own posts.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)