Skip to main content

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:

python
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:

python
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:

python
# 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:

python
# 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:

python
# 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.

python
# 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

python
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:

python
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 via POST /api/books/{pk}/mark_as_read/
  • newest is a custom action that applies to the collection (detail=False), accessible via GET /api/books/newest/

Permissions and Authentication

You can add permissions to your ViewSet just like you would with a regular view:

python
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:

python
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:

python
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:

python
# 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)
python
# 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']
python
# 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)
python
# 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:

  1. Reduce code duplication
  2. Create consistent API interfaces
  3. Simplify URL configuration with routers
  4. Easily add custom actions to resources
  5. Apply permissions and authentication in a centralized way
  6. 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

  1. Create a simple blog API with a Post model and a PostViewSet that supports CRUD operations.
  2. Add a custom action to your PostViewSet to mark a post as featured.
  3. Implement a CommentViewSet that relates to your Post model.
  4. Add filtering and search capabilities to your ViewSets.
  5. 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! :)