What You Should Know About DRF, Part 3: Adding custom endpoints

This is Part 3 of a 3-part series on Django REST Framework viewsets. Read Part 1: ModelViewSet attributes and methods and Part 2: Customizing built-in methods.

I gave this talk at PyCascades 2021 and decided to turn it into a series of blog posts so it's available to folks who didn't attend the conference or don't like to watch videos. Here are the slides and the video if you want to see them.


Sometimes the endpoints you get when you use a ModelViewSet aren't enough and you need to add extra endpoints for custom functions. To do this, you could use the APIView class and add a custom route to your urls.py file, and that would work fine.

But if you have a viewset already, and you feel like this new endpoint belongs with the other endpoints in your viewset, you can use DRF's @action decorator to add a custom endpoint. This means you don't have to change your urls.py -- the method you decorate with your @action decorator will automatically be rendered along with the other enpdoints.

Let's continue with the library example from Part 1. Now we need a new endpoint just for featured books, books with featured = True on the Book model. To do this, we'll add a featured() method to our BookViewSet and decorate with DRF's @action decorator.

from rest_framework import status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet

from .models import Book
from .serializers import BookDetailSerializer, BookListSerializer


class BookViewSet(ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookDetailSerializer

    def get_serializer_class(self):
        if self.action in ["list", "featured"]:
            return BookListSerializer
        return super().get_serializer_class()

    @action(detail=False, methods=["get"])
    def featured(self, request):
        books = self.get_queryset().filter(featured=True)
        serializer = self.get_serializer(books, many=True)
        return Response(serializer.data, status=status.HTTP_200_OK)

This code will result in this endpoint:

GET /books/featured/

Let's start with the @action decorator.

The @action decorator takes a couple of required arguments:

  • detail: True or False, depending on whether this endpoint is expected to deal with a single object or a group of objects. Since we want to return a group of featured books, we have set detail=False.
  • methods: A list of the HTTP methods that are valid to call this endpoint. We set ours to ["get"], so if someone tries to call our endpoint with a POST request, they will receive an error. This argument is actually optional and will default to ["get"], but actions are frequently used for POST requests so I wanted to make sure to mention it.

Once we are in our featured() method, we create the queryset by calling get_queryset() and then filtering for featured=True books. Then we get the right serializer from get_serializer(), which will call get_serializer_class().

Notice that I added "featured" to the list of actions that will return BookListSerializer in get_serializer_class(). The name of the action will share the name of the method.

I pass the books queryset into the serializer, then return the data in the Response object along with the correct HTTP status code. (The status will default to HTTP_200_OK if you don't set it, but I set it explicitly to show you that you can.)


In Part 1: ModelViewSet attributes and methods, I covered the attributes and methods that ship with ModelViewSet, what they do, and why you need to know about them.

In Part 2: Customizing built-in methods, I went through some real-world examples for when you might want to override some of ModelViewSet's built-in methods.