Creating API Views with ViewSets and Routers

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:

  1. Function-Based Views (FBVs) using @api_view decorator.
  2. Class-Based Views (CBVs) using APIView.
  3. 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 BookViewSet inherits from ModelViewSet, which provides built-in implementations for all CRUD operations.
  • The queryset defines which objects the view operates on.
  • The serializer_class defines 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:

EndpointHTTP MethodDescription
/books/GETList all books
/books/POSTCreate a new book
/books/<id>/GETRetrieve a specific book
/books/<id>/PUTUpdate a specific book
/books/<id>/DELETEDelete 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 list action is mapped to a GET request on /books/.
  • The create action is mapped to a POST request on /books/.
  • The retrieve action is mapped to a GET request on /books/<id>/.
  • The update action is mapped to a PUT request on /books/<id>/.
  • The destroy action 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:

  1. SimpleRouter – Generates only the basic routes (list, create, retrieve, update, destroy).
  2. 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=&#91;'get'])
def recent(self, request):
    recent_books = Book.objects.order_by('-published_date')&#91;: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 = &#91;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 = &#91;filters.SearchFilter, filters.OrderingFilter]
search_fields = &#91;'title', 'author']
ordering_fields = &#91;'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:

  1. Less Code – Reduces boilerplate by automatically generating CRUD operations.
  2. Consistency – Enforces standard RESTful patterns across all endpoints.
  3. Maintainability – Centralizes logic in a single class for each resource.
  4. Scalability – Easy to extend with filters, pagination, and permissions.
  5. 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

FeatureViewSetAPIView
Boilerplate CodeMinimalRequires manual setup
RoutingAutomatic via RoutersManual URL configuration
Best ForCRUD-style endpointsCustom, non-standard logic
Common ClassModelViewSetAPIView

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 = &#91;'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.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *