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:
- APIViews
- Generic Views
- 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:
- ViewSet
- GenericViewSet
- ReadOnlyModelViewSet
- ModelViewSet
They're mostly built out of the classes you got to know in the previous article in this series:

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:
listcreateretrieve(pk needed)update(pk needed)partial_update(pk needed)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_routeand@detail_routedecorators 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:
The main difference between them is that DefaultRouter includes a default API root view:

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:
- The URL prefix for the views
- 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:
- Used
DjangoObjectPermissionsand didn't need to check object permissions ourself - 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:
- create an item and list all the items
- retrieve, update, and delete a single item
ReadOnlyModelViewSet
ReadOnlyModelViewSet is 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
APIViewand 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:
APIView- concrete views
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:
- Listing all items
- Creating a new item
- 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:

There are three core ways to use them with several sub-possibilities:
- Extending the
APIViewclass- it's possible to use function-based views as well with a decorator
- Using generic views
GenericAPIViewis the baseGenericAPIViewcan be combined with one or more mixins- Concrete view classes already cover combining
GenericAPIViewwith mixins in all the widely-used ways
ViewSetcombines all possible actions into one classGenericViewSetis more powerful than basicViewSetModelViewSetandReadOnlyModelViewSetprovide 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:
- APIViews
- Generic Views
- ViewSets (this article!)
Špela Giacomelli (aka GirlLovesToCode)