Exploring Django ViewSets: Simplifying API Development and Enhancing Code Reusability
A comprehensive guide to Django ViewSets and their advantages over regular views for building efficient and maintainable APIs
If you have ever written API code in Django, you know that it can be a lot of work. You have to create separate views for each HTTP method and manually create URL patterns that reference each of these views. This can be a lot of code to write and maintain, especially as your API grows in complexity.
But what if there was a way to simplify this process of writing API code and still follow Django's convention?
That's where Django ViewSets come in. In this article, we will take a deep dive into Django ViewSets with practical examples to help us understand better.
Prerequisites
Before diving into Django ViewSets, it's important to have a foundational understanding of the following prerequisites:
Python Programming
- Proficiency in Python programming is necessary, as Django is built on Python.
Django Framework:
Familiarity with Django and the Django Rest Framework.
Understanding core concepts, such as models, views, URLs, and serializers
will provide a solid foundation for working with Django ViewSets.
API Development Concepts:
- Have a basic understanding of API development concepts such as RESTful architecture, HTTP methods (GET, POST, PUT, and DELETE), and request/response handling.
It's important to note that you don't need to be an expert in the topics or concepts listed in the prerequisites. With a basic understanding, you'll be well-prepared to explore Django ViewSets.
Understanding Django Viewsets
Viewsets are a powerful tool that can help you write more concise and maintainable API code. They allow you to group related HTTP methods into logical operations, which can make your code easier to understand. For example, let's say you have an API that allows users to create, read, update, and delete products. Without ViewSets, you'll need to write four separate views for each of these operations. This would be a lot of code to write and maintain.
However, with ViewSets, you can group all of these operations into a single viewset. Meaning, into a single set of views. This makes your code much more concise and easier to understand. Additionally, ViewSets can automatically generate URL patterns for your API, which can save you even more time and effort.
Differences between ViewSets and traditional views
There are quite a number of differences between Viewsets and traditional views, making ViewSets more advantageous when working with complex APIs. Here are some key distinctions:
Concise and easy-to-understand code
With ViewSets, you can customize your code when grouping related HTTP methods. This flexibility makes your code not just concise but also easy to understand
Unlike ViewSets, traditional views use generic views to group related HTTP methods, but they abstract away a lot of details on how your API works, making you less able to customize it. This limitation can make your code hard to understand and maintain.
Routing and URL configuration
ViewSets utilize routers to automatically generate URL patterns based on the actions defined in the ViewSets. This simplifies the URL configuration and eliminates the need for mapping URLs to view functions or classes.
💡We will discuss routers and actions later.Traditional views require developers to explicitly define URL patterns for each API endpoint, which can become cumbersome and error-prone, especially when dealing with a large number of endpoints.
Code Reusability
ViewSets promote code reusability by allowing multiple API operations to be defined within a single class without having to duplicate or rewrite the code.
Traditional views often involve writing separate functions or classes for each endpoint, leading to a higher chance of duplication and a less modular code structure.
Exploring the Various ViewSets and their functionalities
In the previous section, we discussed the concept of ViewSets and how they can be used to simplify the process of creating APIs. In this section, we will take a closer look at ViewSets with series of examples to help us understand better.
Let's say we have a simple API that allows users to create, read, update, and delete products. We can create a ViewSet for this API by defining actions.
You can define a custom action or use the default actions ViewSets provides. Here are some examples of default ViewSet actions:
list
This action represents the HTTPGET
method for a ViewSet. It is used to retrieve a list of objects.retrieve
This action represents the HTTPGET
method for a single object. It is used to retrieve a single object by its unique identity (ID).create
This action represents the HTTPPOST
method for a ViewSet. It is used to create a new object.update
This action represents the HTTPPUT
method for a ViewSet. It is used to update an existing object.partial_update
This action represents the HTTPPATCH
method for a ViewSet. It is used to update parts of an existing object.destroy
This action represents the HTTPDELETE
method for a ViewSet. It is used to delete an object.
However, ViewSets come in different types, each designed to handle specific tasks. The main differences between them lie in the actions they define.
Here is a table that summarizes the differences between the different types of ViewSets:
ViewSet Types | Default Actions | Description |
ModelViewSet | list , retrieve , create , update , partial_update , destroy | The most common type of viewset. It provides access to all the default actions |
ReadOnlyModelViewSet | list , retrieve | Only allows users to read data. |
ViewSet | none | actions are defined manually |
GenericViewSet | inherits from mixin classes | Does not provide any actions by default |
Now, let's take a practical look at these types of ViewSets with examples.
ModelViewSet
This is the most commonly used type of ViewSet. It automatically defines all the actions needed to create a standard CRUD operation for a given model, making it a convenient choice for managing data resources.
from rest_framework import viewsets
from .models import Product
from .serializers import ProductSerializer
class ProductModelViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
This code defines a ModelViewSet
that represents the Product
model. The queryset
attribute defines the set of objects that the ViewSet will operate on. The serializer_class
attribute defines the serializer that will be used to serialize and deserialize objects.
In the code above, the ProductModelViewSet
inherits from ModelViewset
, and from the previous table, we saw that the ModelViewSet
defines all the default actions(list
, retrieve
, create
, update
, partial_update
, destroy
) without us having to manually define it. This means as small as this line of code is, you can send a request to:
get all the products
get a single product
update a product
create a new product
delete a product
So when you send a request, the corresponding action automatically handles that request. For example, if you send a post
request to ProductModelViewSet
, the create
action automatically handles this request and creates a new product. This is the same for every other action the ModelViewSet defines by default.
ReadOnlyModelViewSet
This type of ViewSet is similar to ModelViewSet
, but it only provides read-only actions (list
and retrieve
) for a given model. It is useful for APIs that only need to allow users to read data without modifying it.
from rest_framework import viewsets
from .models import Product
from .serializers import ProductSerializer
class ProductReadOnlyViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
The ReadOnlyModelViewSet
class is a subclass of the ModelViewSet
class. It inherits all of the functionality of the ModelViewSet
class, but it only exposes the list
and retrieve
actions. This means that the create
, update
, partial_update
, and destroy
actions are not available. So you can only send a get
request to retrieve a single product or all the products.
ViewSet
TheViewSet
base class does not provide any actions by default, which means you will define the actions manually. This might look disappointing but it actually gives an extra level of flexibility and allows us to tailor our API endpoints to specific requirements. Unlike the ModelViewSet
, which automatically generates actions based on our models, the ViewSet
base class gives us the freedom to define our methods for each action.
from .models import Product
from .serializers import ProductSerializer
from django.shortcuts import get_object_or_404
from rest_framework.response import Response
from rest_framework.viewsets import ViewSet
class ProductViewSet(ViewSet):
queryset = Product.objects.all()
# Define the list action
def list(self, request):
serializer = ProductSerializer(self.queryset, many=True)
return Response(serializer.data)
# Define the retrieve action
def retrieve(self, request, pk=None):
product = get_object_or_404(self.queryset, pk=pk)
serializer = ProductSerializer(product)
return Response(serializer.data)
In this example, we manually defined two actions. We can as well define the other actions but for example sake, we defined only the list
and retrieve
actions. The list
action is used to retrieve a list of products while the retrieve
action is used to retrieve a single product by its primary key (pk). You can add other actions to perform more operations such as creating, updating, and deleting data.
The ViewSet
base class gives you complete control over how each action is handled, as you are required to implement the methods for each action manually.
GenericViewSet
The GenericViewSet
is very similar to the ViewSet
base class, but it provides the option to define actions manually or not. This means that, unlike the ViewSet
base class, you can use the GenericViewSet
without defining actions manually. The twist here is that GenericViewSet
itself does not provide the actions straightaway like the ModelViewSet
. Instead, it makes use of mixins.
Let us see how we can use GenericViewSets
with Mixins:
from rest_framework import mixins, viewsets
from .models import Product
from .serializers import ProductSerializer
class ProductGenericViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
In this example, we imported only two mixins, ListModelMixin
and RetrieveModelMixin
. This way, we have access to the list
and retrieve
action so we are able to list all the products or retrieve a single product without defining the actions manually. The only difference here is that we had to import the mixins that define the actions we want to make use of which we didn't have to do with ModelViewSet
.
However, GenericViewSet
also provides the option of defining actions manually as in a ViewSet
. Let us look at an example of how to use GenericViewSet
by defining actions manually:
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
from .models import Product
from .serializers import ProductSerializer
class ProductGenericViewSet(GenericViewSet):
serializer_class = ProductSerializer
queryset = Product.objects.all()
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)
def destroy(self, request):
product = self.get_object()
product.delete()
return Response({"message":"product deleted successfully"})
In this example, we manually defined the create
and destroy
actions that handle post requests to create a new product as well as delete requests to destroy a product respectively.
Creating custom actions
Unlike ModelViewSet
and ReadOnlyModelViewSet
, the GenericViewSet
and the ViewSet
base class provides the option to define your own action.
Let us look at an example using the ViewSet
base class to understand what this means.
from rest_framework.viewsets import ViewSet
from rest_framework.response import Response
from rest_framework.decorators import action
from .models import Product
from .serializers import ProductSerializer
class ProductViewSet(ViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
def list(self, request):
# Define the retrieve action
def retrieve(self, request, pk=None):
# Define the retrieve action
@action(detail=True, methods=['GET'])
def product_name(self, request, pk):
"""
This is a custom action that returns the product's name.
"""
product = self.get_object(pk)
return Response({'name': product.name})
This ViewSet has a custom action called product_name
. The action takes two arguments: the request object and the object's primary key (pk). The action then returns the product's name.
The @action
decorator is used to define the custom action. The detail
argument specifies whether the action is a detail action or a list action.
In this example, detail=True
. This means that the action will return the name of a single product object. The methods
argument specifies that the action can only be invoked with the GET
HTTP method.
Generally, the custom action method returns a Response
object with the product's name.
Routing
We can't emphasize the beauty of ViewSets without talking about the automatic generation of URL patterns. Routing in ViewSet is the process of mapping HTTP requests to actions.
The default routing for ViewSet is provided by the router class. The router
class automatically generates URL patterns for all of the actions that are defined in the ViewSet.
For example, if you have a ViewSet with actions like list
, retrieve
, create
, update
, destroy
.
The router
class will automatically generate the following URL patterns:
/
This URL pattern matches thelist
action./<pk>/
This URL pattern matches theretrieve
action./create/
This URL pattern matches thecreate
action./<pk>/update/
This URL pattern matches theupdate
action./<pk>/destroy/
: This URL pattern matches thedestroy
action.
Let us take a look at an example using the ProductModelViewSet
we defined earlier on:
from django.urls import path, include
from rest_framework import routers
from views import ProductModelViewSet
router = routers.DefaultRouter()
router.register(r'product', ProductModelViewSet)
urlpatterns = [
path('api/v1', include(router.urls)),
]
Let's analyse the components of this code for better understanding:
router
: Therouter
is an instance of the DefaultRouter class provided by Django REST Framework. A router is responsible for automatically generating URL patterns for the API views and wiring them to the correct actions..register(...)
: This method is used to register a ViewSet with the router. By callingrouter.register(...)
, we indicate that we want to associate the specified ViewSet,ProductModelViewSet
in this case, with certain URL patterns based on its actions.r'product'
: This is the URL path for the ViewSet's API endpoints. In this case, it uses the string'product'
as the base URL path. When this ViewSet is registered with the router, the router will automatically generate URL patterns based on this path.ProductModelViewSet
: This is the ViewSet class that we want to associate with the URL patterns. In this example, theProductModelViewSet
is the ViewSet class responsible for handling API operations related to the "Product" model.
Putting it all together, when we use router.register(r'product', ProductModelViewSet)
, since the ProductModelViewSet
has actions like list
, retrieve
, create
, update
, and destroy
, the router will automatically generate URL patterns for each of these actions based on the provided base URL path. The router will also take note of the pattern specified here, urlpatterns = [path('api/v1', include(router.urls)), ]
. It then uses all these to generate the URL pattern automatically in the following way:
api/v1/product/
api/v1/product/<pk>/
api/v1/product/create/
api/v1/product/<pk>/update/
api/v1/product/<pk>/destroy/
This automatic URL pattern generation saves us the effort of manually defining URL patterns and connecting them to our ViewSet's actions. By using routers and registering ViewSets, we can build clean and efficient APIs with minimal boilerplate code.
Conclusion
In this article, we have discussed the basics of ViewSets in the Django REST Framework. We have covered the following topics:
Understanding ViewSet.
Exploring the various viewsets and their functionalities.
The different types of ViewSets.
Defining custom actions.
Routing in ViewSets.
We have also provided some code examples to illustrate the concepts that we have discussed.
I hope that this article has given you a good understanding of ViewSets in Django REST Framework. If you have any further questions, please be sure to ask in the comment section, I will do my best to answer it.