Hana Belay
Posted on October 3, 2022
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
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,)
- 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 inurls.py
file like this.
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.
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:
path('profile/', ProfileAPIView.as_view(), name='profile_detail'),
.
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)
- 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)
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)
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, )
- The above class provides you with everything you need for CRUD operations on the
Order
model. In addition, one of the main advantages of usingModelViewSet
, or any other type ofViewSet
, is to have URL endpoints automatically defined for you throughRouters
# 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
, anddestroy
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
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()
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, )
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)
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! 🖤
Posted on October 3, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.