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:
list
create
retrieve
(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_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:
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
DjangoObjectPermissions
and 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
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:
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
APIView
class- it's possible to use function-based views as well with a decorator
- Using generic views
GenericAPIView
is the baseGenericAPIView
can be combined with one or more mixins- Concrete view classes already cover combining
GenericAPIView
with mixins in all the widely-used ways
ViewSet
combines all possible actions into one classGenericViewSet
is more powerful than basicViewSet
ModelViewSet
andReadOnlyModelViewSet
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:
- APIViews
- Generic Views
- ViewSets (this article!)