DEV Community

Cover image for Class-Based Views in DRF are Powerful
Hana Belay
Hana Belay

Posted on

Class-Based Views in DRF are Powerful

If you are starting out with Django Rest Framework, you may feel a bit overwhelmed by the different types of views that exist. However, you will start appreciating how time-saving and powerful the abstractions are once you understand what to use and when.

Here, I have compiled some tips on GenericViews and ViewSets you might want to refer to when building your REST APIs.

Table of Contents

  1. Prerequisite
  2. Introduction
  3. Using GenericViews
    1. get_object(self)
    2. get_queryset(self)
    3. perform_create(self, serializer)
  4. The Power of ViewSets
    1. Different Serializers for Read and Write
    2. Different Permissions for Different Actions
    3. ReadOnlyModelViewSet
  5. Conclusion

Prerequisite

This article assumes that you have a basic understanding on Django and Django Rest Framework (DRF). A decent understanding of serializers and views is also needed.

Introduction

You might be asking yourself, why should I even use class-based views when I can have more control of what is happening in function-based views? This section will answer that and highlight class-based views.

A view is a place where you can put the logic of your application. In general, we have 2 types of views in Django:

  • Function-based views
  • Class-based views

The major problem with function-based views is the lack of an easy way to extend them. You may see yourself using the same kind of code over and over again throughout your project which is not a good design principle.

In addition, function-based views use conditional branching inside a single view function to handle different HTTP methods which might make your code unreadable.

Class-based views aren’t a replacement for function-based views. However, they provide you with a way of writing views in an object-oriented fashion. This means that they can be really powerful and highly extensible by using concepts from OOP such as inheritance and Mixin (multiple inheritance).

Anyways, we have the following class-based views in DRF:

  • APIView
  • GenericView
  • ViewSets

APIView is similar to Django’s View class (It is actually an extension of it). You may have been using this approach to dispatch your requests to an appropriate handler like get() or post().

That being said, let's get started.

Using GenericViews

Can you think of some tasks that you repeat very often while working on a project? Tasks like form handling, list view, pagination, and many other common tasks might make your development experience boring because you repeat the same pattern over and over again. GenericViews come to the rescue by taking certain common patterns and abstracting them so that you can quickly write common views of data and save yourself time for a cup of tea🍵

Here are some of the methods you might often want to override when using GenericViews.

get_object(self)

Assume you want to have a view that will handle a user’s request to retrieve and update their profile.

class ProfileAPIView(RetrieveUpdateAPIView):
    """
    Get, Update user profile
    """
    queryset = Profile.objects.all()
    serializer_class = ProfileSerializer
    permission_classes = (IsUserProfileOwner,)
Enter fullscreen mode Exit fullscreen mode
  • Simple right? you override the queryset attribute to determine the object you want the view to return, as well as your serializer class and permission class. Then, you define the path in urls.py file like this.
path('profile/<int:pk>/', ProfileAPIView.as_view(), name='profile_detail'),
Enter fullscreen mode Exit fullscreen mode
  • This is the kind of pattern you may have used to achieve the goal. However, another way of doing the same thing is by overriding the get_object(self) method to return a profile instance without having to provide a lookup field (<int:pk>) in your path.
class ProfileAPIView(RetrieveUpdateAPIView):
    """
    Get, Update user profile
    """
    queryset = Profile.objects.all()
    serializer_class = ProfileSerializer
    permission_classes = (IsUserProfileOwner,)

    def get_object(self):
        return self.request.user.profile
Enter fullscreen mode Exit fullscreen mode

This way, you can modify urls.py file to remove <int:pk> from the path:

path('profile/', ProfileAPIView.as_view(), name='profile_detail'),
.
Enter fullscreen mode Exit fullscreen mode

get_queryset(self)

Want to return a queryset that is specific to the user making a request? You can do so using get_queryset(self) method

class OrderListCreate(ListCreateAPIView):
    """
    List, Create orders of a user
    """
    queryset = Order.objects.all()
    permission_classes = (IsOrderByBuyerOrAdmin, )

    def get_queryset(self):
        res = super().get_queryset()
        user = self.request.user
        return res.filter(buyer=user)
Enter fullscreen mode Exit fullscreen mode
  • The get_queryset(self) method filters the response to include a list of orders of the currently authenticated user.

perform_create(self, serializer)

Assume you have a Recipe class. When users want to create a recipe, you need to hide the author field in your serializer:

author = serializers.PrimaryKeyRelatedField(read_only=True)
Enter fullscreen mode Exit fullscreen mode

and then in your views, you can automatically set author to the currently authenticated user by overriding the perform_create(self, serializer) method.

class RecipeCreateAPIView(CreateAPIView):
    """
    Create: a recipe
    """
    queryset = Recipe.objects.all()
    serializer_class = RecipeSerializer
    permission_classes = (IsAuthenticated,)

    def perform_create(self, serializer):
        serializer.save(author=self.request.user)
Enter fullscreen mode Exit fullscreen mode

Similar to perform_create(self, serializer), there are also perform_update(self, serializer) and perform_destroy(self, serializer) methods.

The Power of ViewSets

ViewSet is a type of class-based view that combines the logic for a set of related views into a single class. The 2 most common types of ViewSets that you are most likely to use are Modelviewset and ReadOnlyModelViewSet

Say you want to perform CRUD operations on a user's order. Using ModelViewSet, doing so is as simple as:

class OrderViewSet(ModelViewSet):
    """
    CRUD orders of a user
    """
    queryset = Order.objects.all()
    serializer_class = (SomeSerializer, )
        permission_classes = (SomePermission, )
Enter fullscreen mode Exit fullscreen mode
  • The above class provides you with everything you need for CRUD operations on the Order model. In addition, one of the main advantages of using ModelViewSet, or any other type of ViewSet, is to have URL endpoints automatically defined for you through Routers
# urls.py

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

from orders.views import OrderViewSet

app_name = 'orders'

router = DefaultRouter()
router.register(r'', OrderViewSet)

urlpatterns = [
    path('', include(router.urls)),
]
Enter fullscreen mode Exit fullscreen mode
  • With just that, you have URL endpoints for list, create, retrieve, update, and destroy actions!

Different Serializers for Read and Write

Ever needed to separate your serializer for read and write operation? Perhaps because you have a lot of nested fields, but you only need a few of them for write operation? You can easily use a single view for both serializers by overriding the get_serializer_class(self) method.

class ProductViewSet(ModelViewSet):
    """
    CRUD products
    """
    queryset = Product.objects.all()

    def get_serializer_class(self):
        if self.action in ('create', 'update', 'partial_update', 'destroy'):
            return ProductWriteSerializer

        return ProductReadSerializer
Enter fullscreen mode Exit fullscreen mode

Different Permissions for Different Actions

get_permissions(self) method helps you separate permissions for different actions inside the same view.

def get_permissions(self):
    if self.action in ("create", ):
        self.permission_classes = (permissions.IsAuthenticated, )
    elif self.action in ('update', 'partial_update', 'destroy'):
        self.permission_classes = (IsSellerOrAdmin, )
    else:
        self.permission_classes = (permissions.AllowAny, )

    return super().get_permissions()
Enter fullscreen mode Exit fullscreen mode

Note: You can use methods like get_queryset(self), perform_create(self, serializer) et cetera inside Vewsets as well.

ReadOnlyModelViewSet

If you plan to make your view read-only, you can use ReadOnlyModelViewSet

class ProductCategoryViewSet(ReadOnlyModelViewSet):
    """
    List and Retrieve product categories
    """
    queryset = ProductCategory.objects.all()
    serializer_class = ProductCategoryReadSerializer
    permission_classes = (permissions.AllowAny, )
Enter fullscreen mode Exit fullscreen mode

Conclusion

In general, you can see that ViewSets have the highest level of abstraction and you can use them to avoid writing all the code for basic and repetitive stuff. They are a huge time-saver! However, if you need to have more control or do some custom work, using APIView or GenericAPIView makes sense.

For instance, in the following code, I’m using APIView to create a Stripe checkout session. I think this is a good candidate for using APIView

class StripeCheckoutSessionCreateAPIView(APIView):
    """
    Create and return checkout session ID for order payment of type 'Stripe'
    """
    permission_classes = (IsPaymentForOrderNotCompleted,
                          DoesOrderHaveAddress, )

    def post(self, request, *args, **kwargs):
        order = get_object_or_404(Order, id=self.kwargs.get('order_id'))

        order_items = []

        for order_item in order.order_items.all():
            product = order_item.product
            quantity = order_item.quantity

            data = {
                'price_data': {
                    'currency': 'usd',
                    'unit_amount_decimal': product.price,
                    'product_data': {
                        'name': product.name,
                        'description': product.desc,
                        'images': [f'{settings.BACKEND_DOMAIN}{product.image.url}']
                    }
                },
                'quantity': quantity
            }

            order_items.append(data)

        checkout_session = stripe.checkout.Session.create(
            payment_method_types=['card'],
            line_items=order_items,
            metadata={
                "order_id": order.id
            },
            mode='payment',
            success_url=settings.PAYMENT_SUCCESS_URL,
            cancel_url=settings.PAYMENT_CANCEL_URL
        )

        return Response({'sessionId': checkout_session['id']}, status=status.HTTP_201_CREATED)
Enter fullscreen mode Exit fullscreen mode

P.S. Check this out https://www.cdrf.co/ to get a reference of all the methods and attributes of any class-based view in DRF.

The snippets are taken from my projects on GitHub. You can check them out.

Happy coding! 🖤

Top comments (0)