A comprehensive guide to API Views in Django REST Framework – Part 1

This guide takes a comprehensive look at DRF’s API Views. It is divided in three parts. The first part focuses on APIView class.
Total
0
Shares
A comprehensive guide to APIViews in DRF

This guide takes a comprehensive look at DRF’s API Views. It does so in three steps and therefore is divided into three parts accordingly:

  • The first part covers the basics of API Views, such as what theyare, their purpose, and how to code them.
  • The second part explains GenericAPIView and other classes provided by DRF for working with database models and making your life easier in general.
  • The third part reverse-engineers what happens inside an API View during a user request. Knowing this comes in handy when creating more complex, project-tailered solutions (we all come across them sooner or later). The third part also has a visual diagram showing the logic flow. Personally, I love visual stuff.

What is APIView in DRF?

API Views are perhaps the most crucial element of the Django REST Framework. They are responsible for the majority of request processing logic, although they delegate most of the work to other components.

To put it simply, API Views handle incoming user requests, generate an appropriate response, and ensure that necessary checks and balances are done along the way. Those include:

  • User authentification and authorisation
  • Checking and applying API throttles
  • Data serialisation and deserealisation
  • Fetching from and saving to the database
  • Raising validation and other conditional errors

API Views are an extraordinary example of good OOP (Object-oriented Programming) design. They abstract (i.e. hide) most of the processing logic but leave you a handful of properties to easily configure their behaviour in the desired way.

More often than not, you will configure the behaviour of your API Views via the settings.py file. The settings you define there will be applied to all of them.

However, you have to create request-specific logic for each API View individually. After all, each of them is meant to serve a specific purpose. This is done by declaring methods like get() and post() which are often called handlers.

How to use API Views?

Let’s review a few code samples that demonstrate API Views in action.

Pre-requisites and assumptions

To keep this article concise and relevant, I’m going to make a few assumptions:

  • 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, and optionally, you have virtualenv, venv or pipenv (my favourite) on your system and you run all commands within a virtual environment (it’s good practice)
  • You’ve created an empty playground project (if not, you can follow this cheat sheet)

Example of GET request handler

Let’s look at this simple example of API View that handles an incoming GET request and returns a random number between 1 and 10:

# This is how the basic APIView class is imported
from rest_framework.views import APIView
# DRF provides its own Response object which we will
# use in place of Django's standard HttpResponse
from rest_framework.response import Response
# Our example also requires randint
from random import randint


class RandomNumberAPIView(APIView):

    # We declare a `get()` method, which handles the respective request.
    # It has a `request` param, which contains information about the request.
    # ---
    # Note also the `format=None` param, which determines the output format.
    # For example, it can be HTML or JSON. It's mainly used for Browsable API,
    # so you don't need to worry about it, but you should keep it.
    def get(self, request, format=None):
        # Generate random number
        number = randint(1, 10)
        # Make the output more verbose by wrapping it into a dict
        response = {'random_number': number}
        # Pass our dict to Response object - it will handle serialisation
        return Response(response)

Next, we need to register our view in the urls.py file:

from django.urls import path
# NOTE: Import the view from the file where you saved it
from api.views import RandomNumberAPIView


urlpatterns = [
    # Register our view
    path('api/random-number/',
         RandomNumberAPIView.as_view(),
         name='random-number'),
]

Now, if you run the solution, open a console and request the endpoint with curl you should see a response like this:

curl -X GET "http://127.0.0.1:8000/api/random-number/" \
-H  "accept: application/json"
# {"random_number": 3}

Example of POST request handler

Similarly, if we want to handle a POST request, we must declare a post() method. Let it accept two parameters – min and max – and return a random number between them.


    # ... get() method declaration ends here

    # We declare a respecitve method called `post()`
    def post(self, request, format=None):
        # Note how we access the `data` property of the `request` param.
        # It contains the values submitted in the body of the request.
        min_num = request.data.get('min')
        max_num = request.data.get('max')

        # Intentionally skipping validation here for simplicity
        number = randint(min_num, max_num)        
        response = {'random_number': number}
        return Response(response)

Now making a curl request and its response should look like the following:

curl -X POST "http://127.0.0.1:8000/api/random-number/" \
-H  "accept: application/json" \
-H  "Content-Type: application/json" \
-d "{\"min\": 3, \"max\": 9}"
# {"random_number": 8}

Accepting URL path parameters

But what if we want to accept URL path parameters in our view? In other words, what if we wish our endpoint URL to be like this:

http://127.0.0.1:8000/api/random-number/min/:int/max/:int/

Well, we can do that too. It’s done in the same way regular Django views do it. We simply need to let our view handler know which parameters to expect and then use them in the code:

# ... Imports are here

class RandomNumberAPIView(APIView):

    def get(self, request, min, max, format=None):
        number = randint(min, max)
        # ... The rest of the handler's logic is here

And we need to specify those parameters in the URL path of our endpoint inside the urls.py file:

# ... Imports are here

urlpatterns = [
    path('api/random-number/min/<int:min>/max/<int:max>/',
         # ... The rest of the file is the same

Adding policies to API Views

API Views let you easily apply so-called policies or configure their behaviour as I mentioned earlier. These can be the user authentication method, user permission model, request parsers, API throttles, response renderers, etc.

Typically, you specify them inside of the settings.py file. But in some cases, you might want an endpoint to behave differently. For example, a user login endpoint wouldn’t require authentication, while the rest of the API would.

Policies are added via special class properties shown below. They take priority over the directives provided in settings.py.

# ... Other imports go here
# ---
# Import modules containing classes for API policies
from rest_framework import (
    authentication, 
    permissions, 
    parsers, 
    throttling, 
    renderers,
)


class RandomNumberAPIView(APIView):
    authentication_classes = [authentication.TokenAuthentication]
    permission_classes = [permissions.AllowAny]
    parser_classes = [parsers.JSONParser]
    throttle_classes = [throttling.UserRateThrottle]
    renderer_classes = [renderers.JSONRenderer]

    # ... `get()` and `post()` methods declaration goes here

Note also that you can specify [] as a value for any of these properties to apply no policy, but it’s not a good idea to do it with parser_classes and renderer_classes fields.

Class-based views vs Function-based views

Not only API Views in Django REST Framework can be created as classes, but as functions too. This approach is often used for simple endpoints.

In order to convert a function into a view, a special decorator @api_view is used. Here’s a code sample showing it in action:

# Here is the special decorator that does the magic
from rest_framework.decorators import api_view
# Like with class-based views, we return DRF's own Response
from rest_framework.response import Response
# Use randint to replicate the earlier example
from random import randint

# NOTE: apply the decorator to the function to convert 
# it into a fully-functional API view.
@api_view()
def get_random_number(request):
    number = randint(1, 10)
    response = {'random_number': number}
    return Response(response)

Pretty neat, isn’t it? But don’t get mesmerised too much – the magic behind this is pretty simple.

Under the hood, the decorator generates an anonymous instance of the APIView class. Then it sets our function as the default handler of it. Thus, function-based views are nothing more than a shortcut for a class-based view.

NOTE: To be more precise, the class generated by the @api_view decorator is called WrappedAPIView. It's a special subclass of APIView but functionally they're almost the same.

Function-based views are registered slightly differently than class-based views – they don’t require the as_view() method call:

from django.urls import path
# NOTE: Import the view function from the file where you saved it
from api.views import get_random_number


urlpatterns = [
    # Register our view
    path('api/random-number/',
         # Note here we simply pass a reference to our function
         get_random_number,
         name='random-number'),
]

If you wanted the function-based view to accept URL path parameters, the same technique would apply here. The function would need to accept the desired parameters, and they’d have to be specified in the URL path of the endpoint.

Handling POST requests

By default, the @api_view decorator will assume that our view is supposed to only accept GET requests. All other requests would return a 405 error. Here is how to make it accept POST requests too:

# ... Imports go here

# Note how we added an array of allowed methods
# as a parameter for the decorator
@api_view(['GET', 'POST'])
def get_random_number(request):
    # The `request` param has `method` property which
    # can tell use what kind of request this is.
    if request.method == 'POST':
        min_num = request.data.get('min')
        max_num = request.data.get('max')
    else:
        # We could do this even simpler by providing a default
        # value upon getting request data, i.e:
        #   request.data.get('min', 1)
        # But let's use if...else to demonstrate `request.method` property
        min_num = 1
        max_num = 10
    # The rest of the logic
    number = randint(min_num, max_num)
    response = {'random_number': number}
    return Response(response)

As you can see, for a simple endpoint like this one, we can actually save quite a few lines of code thanks to function-based views. But with more complex endpoints, you are most likely to benefit from using class-based views.

Applying API policies

Lastly, you can configure the behaviour of your function-based view just as easily. Instead of class properties, you can use additional decorators with respective names as shown in the sample below:

# NOTE: Import desired decorators
from rest_framework.decorators import (
    api_view, 
    authentication_classes, 
    permission_classes, 
    parser_classes, 
    throttle_classes, 
    renderer_classes,
)
# Don't forget about modules containing classes for API policies
from rest_framework import (
    authentication, 
    permissions, 
    parsers, 
    throttling, 
    renderers,
)
# ... Other imports go here

@api_view(['GET', 'POST'])
# NOTE: Policy decorators MUST go AFTER the `@api_view` decorator ^
@authentication_classes([authentication.TokenAuthentication])
@permission_classes([permissions.IsAuthenticated])
@parser_classes([parsers.JSONParser])
@throttle_classes([throttling.UserRateThrottle])
@renderer_classes([renderers.JSONRenderer])
def get_random_number(request):
    # ... Function logic goes here

You can also pass an empty list [] as param to have no policy classes.

When to use APIView class?

One important thing to keep in mind is that APIView is a “low-level” class. It’s the first layer of the superstructure the Django REST Framework builds on top of the standard Django’s View class.

Thus, the APIView class defines a foundational logic for building API endpoints, but it doesn’t have any logic related to working with database models.

For working with database models, DRF provides a subclass of APIView called GenericAPIView as well as a handful of other subclasses. This is where things get really interesting! The second part of this guide, which will be published soon, will cover the topic.

But when does it make sense to use the APIView class? Generally, when your API endpoint doesn’t perform CRUD (Create, Read, Update, Delete) operations on database models. A few examples where it could be applicable are:

  • Login, logout, restore password endpoints
  • Dashboards
  • For serving static files and images
  • Endpoints generating reports or statistics
  • When aggregeting data from external resources (e.g. via third-party APIs)

Conclusion

The structure of view classes in the Django REST Framework is hierarchical. In the first part of this guide, we’ve covered the first and most fundamental class – APIView.

We’ve looked at some code samples demonstrating the use of this class and discussed that it generally makes sense to use it when not operating on database models.

We also touched upon function-based views, which are a handy shortcut to creating class-based views with fewer lines of code (when used correctly).

The next part of this guide will cover GenericAPIView and other subclasses of APIView used for working with database models – something you’re going to do much more often.

Leave a Reply

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

You May Also Like