Django REST Framework Views - ViewSets

Last updated October 1st, 2021

Up to this point, we've covered creating separate views with APIViews and Generic Views. Often, it makes sense to combine the view logic for a set of related views into a single class. This can be accomplished in Django REST Framework (DRF) by extending one of the ViewSet classes.

ViewSet classes remove the need for additional lines of code and, when coupled with routers, help keep your URLs consistent.

--

Django REST Framework Views Series:

  1. APIViews
  2. Generic Views
  3. ViewSets (this article!)

Contents

ViewSets

ViewSet is a type of class-based view.

Instead of method handlers, like .get() and .post(), it provides actions, like .list() and .create().

The most significant advantage of ViewSets is that the URL construction is handled automatically (with a router class). This helps with the consistency of the URL conventions across your API and minimizes the amount of code you need to write.

There are four types of ViewSets, from the most basic to the most powerful:

  1. ViewSet
  2. GenericViewSet
  3. ReadOnlyModelViewSet
  4. ModelViewSet

They're mostly built out of the classes you got to know in the previous article in this series:

DRF ViewSets Overview

ViewSetMixin is a class where all the "magic happens". It's the only class that all four of the ViewSets share. It overrides the as_view method and combines the method with the proper action.

Method List / Detail Action
post List create
get List list
get Detail retrieve
put Detail update
patch Detail partial_update
delete Detail destroy

ViewSet Class

The ViewSet class takes advantage of the APIView class. It doesn't provide any actions by default, but you can use it to create your own set of views:

from django.shortcuts import get_object_or_404
from rest_framework.response import Response
from rest_framework.viewsets import ViewSet

class ItemViewSet(ViewSet):
    queryset = Item.objects.all()

    def list(self, request):
        serializer = ItemSerializer(self.queryset, many=True)
        return Response(serializer.data)

    def retrieve(self, request, pk=None):
        item = get_object_or_404(self.queryset, pk=pk)
        serializer = ItemSerializer(item)
        return Response(serializer.data)

This ViewSet provides the GET HTTP method, mapped to a list action (for listing all instances) and a retrieve action (for retrieving a single instance).

Actions

The following actions are handled by the router class by default:

  1. list
  2. create
  3. retrieve (pk needed)
  4. update (pk needed)
  5. partial_update (pk needed)
  6. destroy (pk needed)

You can also create custom actions with the @action decorator.

For example:

from django.shortcuts import get_object_or_404
from rest_framework.response import Response
from rest_framework.viewsets import ViewSet

class ItemsViewSet(ViewSet):

    queryset = Item.objects.all()

    def list(self, request):
        serializer = ItemSerializer(self.queryset, many=True)
        return Response(serializer.data)

    def retrieve(self, request, pk=None):
        item = get_object_or_404(self.queryset, pk=pk)
        serializer = ItemSerializer(item)
        return Response(serializer.data)

    @action(detail=False, methods=['get'])
    def items_not_done(self, request):
        user_count = Item.objects.filter(done=False).count()

        return Response(user_count)

Here, we defined a custom action called items_not_done.

The allowed HTTP method is a GET.

We've set it explicitly here, but GET is allowed by default.

The methods parameter is optional, whereas the detail parameter is not. The detail parameter should be set as True if the action is meant for a single object or False if it's meant for all objects.

This action is accessible by default at the following URL: /items_not_done. To change this URL, you can set the url_path parameter in the decorator.

If you've been using ViewSets for a while now, you might remember the @list_route and @detail_route decorators instead of @action. These have been deprecated since version 3.9.

Handling URLs

Although you could map the URLs of your ViewSets the same way as you would with other views, this is not the point of ViewSets.

Instead of using Django's urlpatterns, ViewSets come with a router class that automatically generates the URL configurations.

DRF comes with two routers out-of-the-box:

  1. SimpleRouter
  2. DefaultRouter

The main difference between them is that DefaultRouter includes a default API root view:

DRF Browsable API

The default API root view lists hyperlinked list views, which makes navigating through your application easier.

It's also possible to create a custom router.

Routers can also be combined with urlpatterns:

# urls.py

from django.urls import path, include
from rest_framework import routers

from .views import ChangeUserInfo, ItemsViewSet

router = routers.DefaultRouter()
router.register(r'custom-viewset', ItemsViewSet)

urlpatterns = [
    path('change-user-info', ChangeUserInfo.as_view()),
    path('', include(router.urls)),
]

Here, we created a router (using DefaultRouter, so we get the default API view) and registered the ItemsViewSet to it. When creating a router, you must provide two arguments:

  1. The URL prefix for the views
  2. The ViewSet itself

Then, we included the router inside urlpatterns.

This is not the only way to include routers. Refer to the Routers documentation for more options.

During development, a list of items will be accessible at http://127.0.0.1:8000/custom-viewset/ and a single item will be accessible at http://127.0.0.1:8000/custom-viewset/{id}/.

Since we only defined list and retrieve actions in our ItemsViewSet, the only allowed method is GET.

Our custom action will be available at http://127.0.0.1:8000/custom-viewset/items_not_done/.

Here's how the router maps methods to actions:

# https://github.com/encode/django-rest-framework/blob/3.12.4/rest_framework/routers.py#L83

routes = [
        # List route.
        Route(
            url=r'^{prefix}{trailing_slash}$',
            mapping={
                'get': 'list',
                'post': 'create'
            },
            name='{basename}-list',
            detail=False,
            initkwargs={'suffix': 'List'}
        ),
        # Dynamically generated list routes. Generated using
        # @action(detail=False) decorator on methods of the viewset.
        DynamicRoute(
            url=r'^{prefix}/{url_path}{trailing_slash}$',
            name='{basename}-{url_name}',
            detail=False,
            initkwargs={}
        ),
        # Detail route.
        Route(
            url=r'^{prefix}/{lookup}{trailing_slash}$',
            mapping={
                'get': 'retrieve',
                'put': 'update',
                'patch': 'partial_update',
                'delete': 'destroy'
            },
            name='{basename}-detail',
            detail=True,
            initkwargs={'suffix': 'Instance'}
        ),
        # Dynamically generated detail routes. Generated using
        # @action(detail=True) decorator on methods of the viewset.
        DynamicRoute(
            url=r'^{prefix}/{lookup}/{url_path}{trailing_slash}$',
            name='{basename}-{url_name}',
            detail=True,
            initkwargs={}
        ),
    ]

GenericViewSet

While ViewSet extends APIView, GenericViewSet extends GenericAPIView.

The GenericViewSet class provides the base set of generic view behavior along with the get_object and get_queryset methods.

This is how the ViewSet and GenericViewSet classes are created:

# https://github.com/encode/django-rest-framework/blob/3.12.4/rest_framework/viewsets.py#L210
class ViewSet(ViewSetMixin, views.APIView):
   pass


# https://github.com/encode/django-rest-framework/blob/3.12.4/rest_framework/viewsets.py#L217
class GenericViewSet(ViewSetMixin, generics.GenericAPIView):
   pass

As you can see, they both extend ViewSetMixin and either APIView or GenericAPIView. Other than that, there's no additional code.

To use a GenericViewSet class, you need to override the class and either use mixin classes or define the action implementations explicitly to achieve the desired result.

Using GenericViewSet with Mixins

from rest_framework import mixins, viewsets

class ItemViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):

    serializer_class = ItemSerializer
    queryset = Item.objects.all()

This GenericViewSet is combined with the ListModelMixin and RetrieveModelMixin mixins. Since this is a ViewSet, the router takes care of URL-mapping and the mixins provide actions for the list and detail views.

Using GenericViewSet with Explicit Action Implementations

When using mixins, you only need to provide the serializer_class and queryset attributes; otherwise, you will need to implement the actions yourself.

To emphasize the advantage of GenericViewSet vs ViewSet, we'll use a slightly more complicated example:

from rest_framework import status
from rest_framework.permissions import DjangoObjectPermissions
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet

class ItemViewSet(GenericViewSet):
    serializer_class = ItemSerializer
    queryset = Item.objects.all()
    permission_classes = [DjangoObjectPermissions]

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        serializer.save(serializer)

        return Response(serializer.data, status=status.HTTP_201_CREATED)

    def list(self, request):
        serializer = self.get_serializer(self.get_queryset(), many=True)
        return self.get_paginated_response(self.paginate_queryset(serializer.data))

    def retrieve(self, request, pk):
        item = self.get_object()
        serializer = self.get_serializer(item)
        return Response(serializer.data)

    def destroy(self, request):
        item = self.get_object()
        item.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

Here, we created a ViewSet that allows create, list, retrieve, and destroy actions.

Since we extended GenericViewSet, we:

  1. Used DjangoObjectPermissions and didn't need to check object permissions ourself
  2. Returned a paginated response

ModelViewSet

ModelViewSet provides default create, retrieve, update, partial_update, destroy and list actions since it uses GenericViewSet and all of the available mixins.

ModelViewSet is the easiest of all the views to use. You only need three lines:

class ItemModelViewSet(ModelViewSet):
    serializer_class = ItemSerializer
    queryset = Item.objects.all()

Then, after you register the view to the router, you're good to go!

# urls.py

from django.urls import path, include
from rest_framework import routers

from .views import ChangeUserInfo, ItemsViewSet, ItemModelViewSet

router = routers.DefaultRouter()
router.register(r'custom-viewset', ItemsViewSet)
router.register(r'model-viewset', ItemModelViewSet) # newly registered ViewSet

urlpatterns = [
    path('change-user-info', ChangeUserInfo.as_view()),
    path('', include(router.urls)),
]

Now, you can:

  1. create an item and list all the items
  2. retrieve, update, and delete a single item

ReadOnlyModelViewSet

ReadOnlyModelViewSet is a a ViewSet that provides only list and retrieve actions by combining GenericViewSet with the RetrieveModelMixin and ListModelMixin mixins.

Like ModelViewSet, ReadOnlyModelViewSet only needs the queryset and serializer_class attributes to work:

from rest_framework.viewsets import ReadOnlyModelViewSet

class ItemReadOnlyViewSet(ReadOnlyModelViewSet):

    serializer_class = ItemSerializer
    queryset = Item.objects.all()

APIViews, Generic Views, and ViewSets Summary

Series summary:

  • The first and second second articles covered how to create API views by extending APIView and generic views, respectively.
  • In this article, we covered how to create API views with ViewSets.

To understand the views in-depth, we covered all of the building blocks; but, in real life, you'll most likely use one of the following:

  1. APIView
  2. concrete views
  3. ModelViewSet/ReadOnlyModelViewSet

To quickly see the differences between them, let's look at example of all three in action achieving the same thing.

Three endpoints:

  1. Listing all items
  2. Creating a new item
  3. Retrieving, updating, and deleting a single item

Here's how you can accomplish this by extending APIView:

class ItemsList(APIView):

    def get(self, request, format=None):
        items = Item.objects.all()
        serializer = ItemSerializer(items, many=True)
        return Response(serializer.data)

    def post(self, request, format=None):
        serializer = ItemSerializer(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 ItemDetail(APIView):

    def get(self, request, pk, format=None):
        item = get_object_or_404(Item.objects.all(), pk=pk)
        serializer = ItemSerializer(item)

        return Response(serializer.data)

    def put(self, request, pk, format=None):
        item = get_object_or_404(Item.objects.all(), pk=pk)
        serializer = ItemSerializer(item, 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, format=None):
        item = get_object_or_404(Item.objects.all(), pk=pk)
        item.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

Here's how you do the same thing with concrete generic views:

class ItemsListGeneric(ListCreateAPIView):
    queryset = Item.objects.all()
    serializer_class = ItemSerializer


class ItemDetailGeneric(RetrieveUpdateDestroyAPIView):
    queryset = Item.objects.all()
    serializer_class = ItemSerializer

And here are the lines that you need with ModelViewSet:

class ItemsViewSet(ModelViewSet):
    serializer_class = ItemSerializer
    queryset = Item.objects.all()

Finally, here's what the respective URL configurations would look like:

# APIView

from django.urls import path
from views import ItemsList, ItemDetail

urlpatterns = [
    path('api-view', ItemsList.as_view()),
    path('api-view/<pk>', ItemDetail.as_view()),
]


# generic views

from django.urls import path,
from views import ItemsListGeneric, ItemDetailGeneric

urlpatterns = [
    path('generic-view', ItemsListGeneric.as_view()),
    path('generic-view/<pk>', ItemDetailGeneric.as_view()),
]


# ViewSet

from django.urls import path, include
from rest_framework import routers
from views import ItemsViewSet

router = routers.DefaultRouter()
router.register(r'viewset', ItemsViewSet)

urlpatterns = [
    path('', include(router.urls)),
]

Conclusion

DRF Views are a complicated, tangled web:

DRF Views Overview

There are three core ways to use them with several sub-possibilities:

  1. Extending the APIView class
    • it's possible to use function-based views as well with a decorator
  2. Using generic views
    • GenericAPIView is the base
    • GenericAPIView can be combined with one or more mixins
    • Concrete view classes already cover combining GenericAPIView with mixins in all the widely-used ways
  3. ViewSet combines all possible actions into one class
    • GenericViewSet is more powerful than basic ViewSet
    • ModelViewSet and ReadOnlyModelViewSet provide the most functionality with the smallest amount of code

All of the above provide hooks that allow easy customization.

Most of the time, you'll find yourself using either APIView, one of the concrete view classes, or (ReadOnly)ModelViewSet. That said, understanding how the views are built and the advantages of the ancestors can prove helpful when you're trying to develop a customized solution.

Django REST Framework Views Series:

  1. APIViews
  2. Generic Views
  3. ViewSets (this article!)

Špela Giacomelli (aka GirlLovesToCode)

Špela Giacomelli (aka GirlLovesToCode)

GirlThatLovesToCode is passionate about learning new things -- both for herself and for teaching others. She's a fan of Python and Django and wants to know everything there is about those two. When she’s not writing code or a blog, she’s probably trying something new, reading, or spending time outside with her family.

Share this tutorial

Featured Course

Test-Driven Development with Django, Django REST Framework, and Docker

In this course, you'll learn how to set up a development environment with Docker in order to build and deploy a RESTful API powered by Python, Django, and Django REST Framework.

Featured Course

Test-Driven Development with Django, Django REST Framework, and Docker

In this course, you'll learn how to set up a development environment with Docker in order to build and deploy a RESTful API powered by Python, Django, and Django REST Framework.