Comprehensive guide to GenericAPIViews in Django REST Framework

This part of our comprehensive guide series explains what GenericAPIViews are in Django Rest Framework and how to use them.
Total
0
Shares
A comprehensive guide to GenericAPIViews in DRF

This part of our comprehensive guide series explains what GenericAPIViews are in Django Rest Framework and how to use them. It’s the third part of the series. Here is a quick summary of the whole series:

  • The first part covers the APIView class – the foundation of API endpoints in the Django REST Framework.
  • The second part talks about the Serializer and ModelSerializer classes, and explains their purpose and use.
  • The third part covers GenericAPIView and its sibling classes designed for working with database models. These are the classes you’re going to use most commonly.
  • The fourth part talks about ViewSet class and its subclasses, which allow you to create an entire CRUD set of endpoints with just a few lines of code.
  • The fifth part reverse-engineers the internal mechanics of Django REST Framework and explains what exactly happens during a request.

Pre-requisites and assumptions

  • You have a basic understanding of RESTful APIs.
  • You have a basic knowledge of the Django framework.
  • You have Python 3.6+ installed on your system.
  • You have pip installed and ready to use. Optionally, you have virtualenvvenv or pipenv (my favourite) on your system, and you run all commands within a virtual environment (it’s good practice).
  • You’re continuing the project from the previous part of the series or you created a new empty Django project (feel free to use this cheat sheet as help).

What is GenericAPIView in DRF?

We already reviewed the foundational APIView class in the first part of the series and by now we know that while it provides a base for API endpoints, it lacks request handlers and tools for working with database models.

In reality, an application can hardly exist without a database. And while you can make the APIVIew class interact with database models, you’d have to manually extend it with several methods to do things like the following:

  • Fetch model querysets
  • Look up a single model instance or return a 404 error if it doesn’t exist
  • Interact with a serializer to process and save request data
  • Paginate querysets

You’d most likely end up creating a subclass of APIView containing methods for taking care of the above and you’d reuse it in all your views. Well, GenericAPIView is exactly a class like that!

How to use GenericAPIViews?

There are two main properties you’ll always need to specify when inheriting your view from GenericAPIView. Those are queryset and serializer_class. Let’s look at a quick example of how to set them:

# NOTE: we import `generics` from DRF to get access to `GenericAPIView`
from rest_framework import generics
# Let's also assume we have a model named "Wizard" and its serializer
from .models import Wizard
from .serializers import WizardSerializer

class WizardAPIView(generics.GenericAPIView):
    # The `queryset` property expects a model queryset instance
    queryset = Wizard.objects.all()
    # The serializer class expects a reference to the serializer class
    serializer_class = WizardSerializer
NOTE: In reality we rarely inherit directly from GenericAPIView because it lacks any request handlers, such as get(), post(), etc. There is a number of subclasses of GenericAPIView that have those handlers. We will cover these subclasses further in the article.

As was mentioned earlier, GenericAPIView introduces tools for looking up a single object from the given queryset. By default, it uses the pk field for that.

In order to make use of this functionality, we must capture the pk in the endpoint’s URL. The most common approach for this is to register our view in the following way:

# File: urls.py
urlpatterns = [
    path('api/wizards/<int:pk>/', WizardAPIView.as_view(), name='wizard')
]

But sometimes we might want to look up an instance by a field other than pk. For that, we have to set the lookup_field property on our view class. Note that we would need to change the name of the captured value parameter in the route registration as well:

# File: views.py
class WizardAPIView(generics.GenericAPIView):
    # ... `queryset` and `serializer_class` go here
    lookup_field = 'name'

# File: urls.py
urlpatterns = [
    path('api/wizards/<string:name>', WizardAPIView.as_view(), name='wizard')
]

If for whatever reason we want the captured value parameter to have a different name than our lookup_field we can set the lookup_url_kwarg parameter on our view:

# File: views.py
class WizardAPIView(generics.GenericAPIView):
    # ... `queryset` and `serializer_class` go here
    lookup_field = 'name'
    lookup_url_kwarg = 'known_as'

# File: urls.py
urlpatterns = [
    path('api/wizards/<string:known_as>', 
         WizardAPIView.as_view(), 
         name='wizard')
]

Lastly, it’s worth mentioning that we can specify API policies on our view just like how we did with regular APIViews:

# File: views.py
# ...other imports go here
from rest_framework import permissions

class WizardAPIView(generics.GenericAPIView):
    # ...other properties go here
    authentication_classes = [] # Strip authentication
    permission_classes = [permissions.AllowAny]
    # Etc.

How does GenericAPIView work under the hood?

Under the hood, GenericAPIView introduces a number of methods that implement the functionality we mentioned at the beginning of this article.

While most of them were not meant to be used directly, it’s worth knowing at least some of them – particularly those that you might want to overwrite when working on a complex endpoint.

NOTE: The methods of GenericAPIView represented in this section are somewhat simplified for the sake of example.

Starting with queryset handling, GenericAPIView has a method called get_queryset(), which does exactly what the name suggests. Here is a slightly simplified representation of this method’s logic:

# File: rest_framework/generics.py
class GenericAPIView(views.APIView):
    # ...properties go here

    def get_queryset(self):
        queryset = self.queryset
        if isinstance(queryset, QuerySet):
            # Ensure queryset is re-evaluated on each request.
            queryset = queryset.all()
        return queryset

You might be wondering – what is the purpose of having a method when we already have the queryset property? This allows us to easily overwrite it if we want some custom functionality. For example:

# File: views.py
class WizardAPIView(generics.GenericAPIView):
    # ...properties go here (`queryset` is not needed)

    def get_queryset(self):
        # Filter our queryset if the query parameter is provided
        is_archmage = self.request.query_params.get('is_archmage', False)
        if is_archmage:
            return Wizard.objects.filter(is_archmage=is_archmage)
        return Wizard.objects.all()

Moving on. The GenericAPIView class also has a method for instantiating a serializer class called get_serializer(). Here is how its implementation looks:

# File: rest_framework/generics.py
class GenericAPIView(views.APIView):
    # ...properties and `get_queryset()` go here

    def get_serializer(self, *args, **kwargs):
        serializer_class = self.get_serializer_class()
        kwargs.setdefault('context', self.get_serializer_context())
        return serializer_class(*args, **kwargs)

Note how it makes calls to two other methods: get_serializer_class() and get_serializer_context().

The idea behind get_serializer_class() is the same as get_queryset() method – to allow for easy customisation. For example, you could do something like this:

# File: serializers.py
# Imagine that we have the two following serializers:
class WizardFullSerializer(serializers.ModelSerializer):
    class Meta:
        model = Wizard
        fields = '__all__'

class WizardShortSerializer(serializers.ModelSerializer):
    class Meta:
        model = Wizard
        fields = ('pk', 'name')

# File: views.py
class WizardAPIView(generics.GenericAPIView):
    # ...other properties go here (`serializer_class` is not needed)

    def get_serializer_class(self):
        # Use a different serializer if the query parameter is provided
        show_full = self.request.query_params.get('show_full', False)
        if show_full:
            return WizardFullSerializer
        return WizardShortSerializer

The second method – get_serializer_context() – passes a few properties to the serializer instance. Here is what they are:

# File: rest_framework/generics.py
class GenericAPIView(views.APIView):
    # ...properties and methods go here

    def get_serializer_context(self):
        return {
            # The `Request` instance from this view
            'request': self.request,
            # The value passed to the view via the `?format=...` query param
            'format': self.format_kwarg,
            # A reference to the view instance itself
            'view': self
        }

If you recall from the snippet above, the get_serializer() method passes this context object to the serializer constructor via kwargs under the context key. This means you can access the context in your serializer code like so:

class WizardFullSerializer(serializers.ModelSerializer):
    # ... `Meta` class goes here

    def validate(self, attrs):
        # For example, only run validation on `POST` requests
        if self.context['request'].method == 'POST':
            # Do some validation...
        return attrs 

Last but not least, the GenericAPIView class has the get_object() method that searches a single object in the queryset. Here is a sneak peek into its implementation:

# File: rest_framework/generics.py - the implementation is slighly simplified
class GenericAPIView(views.APIView):
    # ...properties and methods go here

    def get_object(self):
        # Get the queryset
        queryset = self.get_queryset()

        # Perform the lookup filtering
        # Note how it makes use of `lookup_url_kwarg` and `lookup_field`
        # If `lookup_url_kwarg` was not set we default to `lookup_field`
        lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field

        # Build filter kwargs and look up the object; raise 404 if not found
        filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
        obj = get_object_or_404(queryset, **filter_kwargs)

        # Check object-level permissions (don't worry about this for now)
        self.check_object_permissions(self.request, obj)

        return obj  # Return the object

To finish off this section, it’s worth saying that despite the fact that GenericAPIView comes with all these neat functions, we rarely inherit our views directly from it. Let’s see why.

Why don’t we use GenericAPIView directly?

In reality, we almost never subclass our views directly from GenericAPIView because similarly to APIView, it’s an intermediate class that lays out foundational logic for working with database models but lacks request handlers.

However, there are several subclasses of GenericAPIView that contain request handlers, and those are the classes you’re going to use most of the time. Here is a full list of them:

  • ListAPIView
  • CreateAPIView
  • RetrieveAPIView
  • UpdateAPIView
  • DestroyAPIView
  • ListCreateAPIView
  • RetrieveUpdateAPIView
  • RetrieveDestroyAPIView
  • RetrieveUpdateDestroyAPIView

That’s quite a bunch, isn’t it? The names of these subclasses suggest their purpose, but to be crystal clear about how they work let’s refresh a few things about RESTful APIs and see how the Django REST Framework relates to them.

Why so many subclasses of GenericAPIView?

In order to understand why there are so many subclasses of GenericAPIView we should quickly refresh a couple of things about RESTful APIs:

Firstly, RESTful APIs are commonly divided into two buckets by their scope, which can be one of the following:

  • A list of objects
  • A single object

In the Django Rest Framework, these scopes are referred to as list and detail.

Secondly, RESTful APIs make use of HTTP methods to identify operations. The most commonly used request methods are:

  • GET – for fetching a list of objects or a single object
  • POST – for creating new objects
  • PUT and PATCH – for editing existing objects
  • DELETE – for deleting objects

Thus, a collection of endpoints that allows all CRUD operations on a database model (let’s use wizards as an example, yay!) might look like displayed in the table below:

List scope:
-----------
GET    /wizards     - list all wizards of the guild
POST   /wizards     - create a new wizard

Detail scope:
-------------
GET    /wizards/:id - get a single wizard's profile
PUT    /wizards/:id - update a single wizard's profile (entire record)
PATCH  /wizards/:id - updated a single wizard's profile (specific fields)
DELETE /wizards/:id - kick a wizard out of the guild (oh no!)

As you’ve probably realised, the subclasses of GenericAPIView allow us to create each of those endpoints. Let’s now dive into how they do it.

How the subclasses of GenericAPIView work

The main purpose of GenericAPIView subclasses is to provide implementations of request handlers. However, it’s worth mentioning that DRF stores the actual implementations of request handlers as a bunch of “mixins”.

NOTE: In case this sounds new to you, mixins in Python are tiny little classes containing bits of reusable code. They can be "mixed in" when declaring a class thanks to Python's multiple inheritance.

As you will soon see, the subclasses of GenericAPIView make use of DRF’s mixins. Here is a full list of them:

  • ListModelMixin
  • CreateModelMixin
  • RetrieveModelMixin
  • UpdateModelMixin
  • DestroyModelMixin

The names are pretty self-explanatory, but we will dig into each implementation in the next section.

To have a better understanding of the Django REST Framework’s views hierarchy take a moment to study this diagram (I love visual stuff):

DRF GenericAPIView Hierarchy Diagram

Now let’s dive into the code of the subclasses of GenericAPIView and see how they make use of those mixins.

How does ListAPIView work?

As the name suggests, the ListAPIView class allows us to display a list of model objects. It’s a list-scope view that provides the implementation for the GET /wizards endpoint from the table above:

# File: rest_framework/generics.py
class ListAPIView(mixins.ListModelMixin, GenericAPIView):

    def get(self, request, *args, **kwargs):
        return self.list(request, *args, **kwargs)

# File: rest_framework/mixins.py - the implementation is slightly simplified
class ListModelMixin:

    def list(self, request, *args, **kwargs):
        # Get the queryset
        queryset = self.get_queryset()

        # Instantiate the serializer
        serializer = self.get_serializer(queryset, many=True)

        # Return the serialized data
        return Response(serializer.data)

The logic is fairly straightforward: we fetch the queryset, pass it to the serializer, and then return the serialized data.

NOTE: In reality, the list() method also paginates the queryset if the pagination_class is specified either globally or locally on the view.

How does CreateAPIView work?

The next one is another list-scope view – the CreateAPIView class. As the name suggests, it allows us to create new database records and provides the implementation for the POST /wizards endpoint from our table:

# File: rest_framework/generics.py
class CreateAPIView(mixins.CreateModelMixin, GenericAPIView):

    def post(self, request, *args, **kwargs):
        return self.create(request, *args, **kwargs)

# File: rest_framework/mixins.py - the implementation is slightly simplified
class CreateModelMixin:

    def create(self, request, *args, **kwargs):
        # Instantiate the serializer
        serializer = self.get_serializer(data=request.data)

        # Perform validation and respond with error messages if failed
        serializer.is_valid(raise_exception=True)

        # Create a new instance
        self.perform_create(serializer)

        # Return the serialized data
        return Response(serializer.data, status=status.HTTP_201_CREATED)

    def perform_create(self, serializer):
        serializer.save()

A couple of things worth noticing here:

Firstly, the call to serializer’s save() method, which you might recall from the second part of the series, is wrapped into its own method called perform_create(). This is done so that we can easily customise this behaviour by overwriting the method should we ever need to.

Secondly, the Response is returned with a 201 - Created status. This follows the standards of the REST API specification.

How does RetrieveAPIView work?

The RetrieveAPIView class is a detail-scope view. This means it makes use of the get_object() method mentioned earlier. As the name suggests, RetrieveAPIView simply shows the requested object. It provides the implementation of the GET /wizards/:id endpoint from our table:

# File: rest_framework/generics.py
class RetrieveAPIView(mixins.RetrieveModelMixin, GenericAPIView):

    def get(self, request, *args, **kwargs):
        return self.retrieve(request, *args, **kwargs)

# File: rest_framework/mixins.py
class RetrieveModelMixin:

    def retrieve(self, request, *args, **kwargs):
        # Get the model instance
        instance = self.get_object()

        # Instantiate the serializer
        serializer = self.get_serializer(instance)

        # Return the serialized data
        return Response(serializer.data)

The logic is pretty simple: we fetch the queryset and then look up the required instance by its pk or whatever field was specified.

You might recall that if the instance cannot be found, the get_object() method will raise a 404 - Not Found error.

Lastly, we serialize the instance and return the data.

How does UpdateAPIView work?

The UpdateAPIView class is also a detail-scope view that brings the logic for updating model instances to the table. It provides the implementation of the PUT|PATCH /wizards/:id endpoint from the table above:

# File: rest_framework/generics.py
class UpdateAPIView(mixins.UpdateModelMixin, GenericAPIView):

    def put(self, request, *args, **kwargs):
        return self.update(request, *args, **kwargs)

    def patch(self, request, *args, **kwargs):
        # Note how we use `partial_update()` here
        return self.partial_update(request, *args, **kwargs)

# File: rest_framework/mixins.py - the implementation is slightly simplified
class UpdateModelMixin:

    def update(self, request, *args, **kwargs):
        # Check whether this update is partial (partial=PATCH)
        partial = kwargs.pop('partial', False)

        # Get the model instance
        instance = self.get_object()

        # Instantiate the serializer and pass the `partial` arg to it
        serializer = self.get_serializer(
            instance, 
            data=request.data, 
            partial=partial
        )

        # Perform validation and respond with error messages if failed
        serializer.is_valid(raise_exception=True)

        # Update the instance
        self.perform_update(serializer)

        # Return the serialized data
        return Response(serializer.data)

    def partial_update(self, request, *args, **kwargs):
        # Explicitly set `partial` in the kwargs
        kwargs['partial'] = True
        return self.update(request, *args, **kwargs)

    def perform_update(self, serializer):
        serializer.save()

This mixin is perhaps the most complicated of them all. But if looked at closely, the logic is also pretty straightforward:

We check whether the partial argument is passed. If so, we know that this is a PATCH request. However, it’s up to the serializer how to handle this. Our job is to simply let it know.

Next, we look up the model instance with get_object(), which will raise a 404 - Not Found error if the instance cannot be found.

Then, we instantiate the serializer passing the partial argument to it, and after that, we perform the validation.

Once again, the call to the serializer’s save() method is wrapped into a separate method called perform_update(). As usual, this is done to allow us to easily overwrite this method if we ever need to customise the behaviour.

How does DestroyAPIView work?

The last view on our list is the DestroyAPIView class. It provides the implementation of the DELETE /wizards/:id endpoint from our table, which means it’s also a detail-scope view:

# File: rest_framework/generics.py
class DestroyAPIView(mixins.DestroyModelMixin, GenericAPIView):

    def delete(self, request, *args, **kwargs):
        return self.destroy(request, *args, **kwargs)

# File: rest_framework/mixins.py
class DestroyModelMixin:

    def destroy(self, request, *args, **kwargs):
        # Get the model instance
        instance = self.get_object()

        # Simply delete - no need to instantiate the serializer
        self.perform_destroy(instance)

        # Return an empty response
        return Response(status=status.HTTP_204_NO_CONTENT)

    def perform_destroy(self, instance):
        instance.delete()

And this mixin is perhaps the simplest of them all, at least because it doesn’t even use a serializer.

We simply look up the object with get_object() or raise a 404 - Not Found error if the instance cannot be found.

After that, we perform a direct delete() call on the instance, which is wrapped into a separate method called perform_destroy(). Same as in other similar cases, this is done for easy customisation of the behaviour.

Note how we return an empty response with a 204 - No Content status at the end. This follows the REST API specification’s standards.

The remaining GenericAPIView subclasses

All the remaining subclasses are simply a combination of two or three of the mixins explained above:

  • ListCreateAPIView combines ListModelMixin and CreateModelMixin
  • RetrieveUpdateAPIView combines RetrieveModelMixin and UpdateModelMixin
  • RetrieveDestroyAPIView combines RetrieveModelMixin and DestroyModelMixin
  • RetrieveUpdateDestroyAPIView combines RetrieveModelMixin, UpdateModelMixin and DestroyModelMixin

The reason DRF provides these is not only to save us time and effort but also because it’s impossible to register two views under the same route. Consider this example:

# File: views.py
# Imagine that we have the two following views:
class ListWizardAPIView(generics.ListAPIView):
    serializer_class = WizardSerializer
    queryset = Wizard.objects.all()

class CreateWizardAPIView(generics.CreateAPIView):
    serializer_class = WizardSerializer
    queryset = Wizard.objects.all()

# File: urls.py
# The following WILL NOT WORK:
urlpatterns = [
    path(
        '/api/wizards',
        views.ListWizardAPIView.as_view(),
        name='wizard-list',
    ),
    path(
        '/api/wizards',
        views.CreateWizardAPIView.as_view(),
        name='wizard-create',
    )
]

If you register two views like in the example above and try to make a POST call to /api/wizards you will receive a 405 - Method Not Allowed error. This is because of how Django registers views: only the first one will be registered.

Thus, having several request handlers bundled together allows us to register them under the same route:

# File: urls.py
urlpatterns = [
    # List scope
    path(
        '/api/wizards',
        views.ListCreateWizardAPIView.as_view(),
        name='wizard-list',
    ),
    # Detail scope
    path(
        '/api/wizards/:id',
        views.RetrieveUpdateDestroyWizardAPIView.as_view(),
        name='wizard-detail',
    )
]

Conclusion

In this chapter of the comprehensive guides series we had a detailed look at the GenericAPIView class and its subclasses.

We learned that it comes with methods for fetching a database queryset and looking up a specific model instance, as well as methods for working with a model serializer.

We also explored the hierarchy of views in Django REST Framework and learned about the mixin classes that provide implementations of request handlers, and that the subclasses of GenericAPIView are made with those mixins.

These mixin classes are also used in the ViewSet class declaration, which we are going to explore in the next part of the series.

Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like