Exploring Django ViewSets: Simplifying API Development and Enhancing Code Reusability

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:

  1. Python Programming

  2. Django Framework:

  3. 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.

💡
An action is a class method defined within a ViewSet to handle the requests sent to a specific API endpoint. They act precisely like HTTP methods.

You can define a custom action or use the default actions ViewSets provides. Here are some examples of default ViewSet actions:

  • listThis action represents the HTTP GET method for a ViewSet. It is used to retrieve a list of objects.

  • retrieveThis action represents the HTTP GET method for a single object. It is used to retrieve a single object by its unique identity (ID).

  • createThis action represents the HTTP POST method for a ViewSet. It is used to create a new object.

  • updateThis action represents the HTTP PUT method for a ViewSet. It is used to update an existing object.

  • partial_updateThis action represents the HTTP PATCH method for a ViewSet. It is used to update parts of an existing object.

  • destroyThis action represents the HTTP DELETE 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 TypesDefault ActionsDescription
ModelViewSetlist, retrieve, create, update, partial_update, destroyThe most common type of viewset. It provides access to all the default actions
ReadOnlyModelViewSetlist, retrieveOnly allows users to read data.
ViewSetnoneactions are defined manually
GenericViewSetinherits from mixin classesDoes 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.

💡
Mixins are pre-built classes that you can inherit from to add functionality to your own classes. To learn more about mixins, see here.

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.

💡
A detail action returns a single object while a list action returns a list of objects.

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 the list action.

  • /<pk>/This URL pattern matches the retrieve action.

  • /create/This URL pattern matches the create action.

  • /<pk>/update/This URL pattern matches the update action.

  • /<pk>/destroy/: This URL pattern matches the destroy 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:

  1. router: The router 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.

  2. .register(...): This method is used to register a ViewSet with the router. By calling router.register(...), we indicate that we want to associate the specified ViewSet, ProductModelViewSet in this case, with certain URL patterns based on its actions.

  3. 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.

  4. ProductModelViewSet: This is the ViewSet class that we want to associate with the URL patterns. In this example, the ProductModelViewSet 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.

💡
You can still define the URLs manually as you would in a traditional view if the automatic URL routing provided by routers doesn't fully meet your requirements.

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.

References

Django Rest Framework Docs

Django official documentation

Â