Class-Based Views in DRF are Powerful
Tags: webdev, python, django, tutorial
Published: 2022-10-03 06:31:15

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](#prerequisite)
2. [Introduction](#introduction)
3. [Using GenericViews](#generic)
    1. [`get_object(self)`](#get_object)
    2. [`get_queryset(self)`](#get_queryset)
    3. [`perform_create(self, serializer)`](#perform_create)
4. [The Power of ViewSets](#viewsets)
    1. [Different Serializers for Read and Write](#get_serializer_class)
    2. [Different Permissions for Different Actions](#get_permissions)
    3. [ReadOnlyModelViewSet](#read_only)
4. [Conclusion](#conclusion)

## Prerequisite <a name="prerequisite"></a>
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 <a name="introduction"></a>

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 <a name="generic"></a>

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:tea:

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

### `get_object(self)` <a name="get_object"></a>

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

```python
class ProfileAPIView(RetrieveUpdateAPIView):
    """
    Get, Update user profile
    """
    queryset = Profile.objects.all()
    serializer_class = ProfileSerializer
    permission_classes = (IsUserProfileOwner,)
```

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

```python
path('profile/<int:pk>/', ProfileAPIView.as_view(), name='profile_detail'),
```

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

```python
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
```

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

```python
path('profile/', ProfileAPIView.as_view(), name='profile_detail'),
.
```

### `get_queryset(self)` <a name="get_queryset"></a>

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

```python
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)
```

- The `get_queryset(self)` method filters the response to include a list of orders of the currently authenticated user.

### `perform_create(self, serializer)` <a name="perform_create"></a>

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

```python
author = serializers.PrimaryKeyRelatedField(read_only=True)
```

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

```python
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)
```
Similar to `perform_create(self, serializer)`, there are also `perform_update(self, serializer)` and `perform_destroy(self, serializer)` methods.

## The Power of ViewSets <a name="viewsets"></a>

`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:

```python
class OrderViewSet(ModelViewSet):
    """
    CRUD orders of a user
    """
    queryset = Order.objects.all()
    serializer_class = (SomeSerializer, )
		permission_classes = (SomePermission, )
```

- 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`

```python
# 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)),
]
```

- With just that, you have URL endpoints for `list`, `create`, `retrieve`, `update`, and `destroy` actions!

### Different Serializers for Read and Write <a name="get_serializer_class"></a>

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.

```python
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
```

### Different Permissions for Different Actions <a name="get_permissions"></a>

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

```python
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()
```

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

### ReadOnlyModelViewSet <a name="read_only"></a>

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

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

## Conclusion <a name="conclusion"></a>

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`

```python
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)
```

*P.S. Check this out [https://www.cdrf.co/](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](https://github.com/earthcomfy). You can check them out.

Happy coding! 🖤