Comprehensive guide to APIViews in Django REST Framework

This article contains a comprehensive review of APIViews in the Django REST Framework, along with examples of how to use them.
Total
0
Shares
A comprehensive guide to APIViews in DRF

This article contains a comprehensive review of APIViews in the Django REST Framework, along with examples of how to use them. It’s the first part of the comprehensive guides 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 created a new empty Django project (feel free to use this cheat sheet as help).

What is APIView in DRF?

APIViews 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 this work to other components.

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

To summarise their role, we can say that APIViews handle incoming user requests, generate an appropriate response, and ensure that all necessary procedures are done along the way, which include:

  • User authentification and authorisation
  • Checking and applying API throttles
  • Data serialisation and deserealisation
  • Fetching from and saving to the database
  • Performing validation and displaying appropriate error messages to the user

How to use APIViews?

Just like with views in the Django framework itself, views in the Django REST Framework can be declared in one of two ways: either as classes or as functions.

We will begin with class-based views because they’re more commonly used than function-based views, plus their code is quite simple and easy to follow.

To declare a DRF view we will need to inherit from the APIView class. It’s a common practice to place all of your APIViews inside the views.py file (or inside a “views” folder if you decide to split up your views into individual python files).

The logic for handling specific requests, like GET and POST, is declared inside respective methods, like get() and post(). These are often called request handlers. Let’s look at a few code samples that demonstrate them in action.

Example of GET request handler

Below is a simple example of an APIView that handles incoming GET requests and returns a random number between 1 and 10. Add the following code to the views.py file of your project:

# 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 to generate random numbers
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 let Django know about our view and choose what URL it will be available at. We’re going to register our view in the urls.py file:

from django.urls import path
# Note how we import our view here
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 with the Django’s runserver command, and open a terminal window, you’ll be able to request our endpoint with curl:

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

You can also check our view directly in the browser. The Browsable API will show you a nice UI with the details about our endpoint.

Example of POST request handler

If we want our endpoint to be able to handle POST requests in addition to GET requests, we can add a post() method to our APIView.

Since POST requests allow us to receive some data from users, let’s make use of that. Our post handler will accept two parameters – min and max – and return a random number between them:

    # ... get() method declared 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)

Let’s test the result with curl by making a POST request in the terminal window and see what the response is looking like:

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}

Like in the GET example, you can open the URL directly in your browser and do the testing via the Browsable API.

Configuring global API settings

At this point, let’s make a little step back and talk about some fundamentals.

When starting a fresh new project, you’ll most likely want to begin with configuring the overall behaviour of your API before you create any views. This is done in the settings.py file.

You’ll need to decide on so-called policies for your API. These include whether your API is publically available or requires users to log in, whether you use JSON, XML or some other format for requests and responses, and so on.

The snippet below contains the settings which suit our tutorial project. If you’re curious, you can find more comprehensive explanations with options to choose from in this cheat sheet under the “Configure DRF settings” section.

INSTALLED_APPS = [
    # ...Django core apps go here
    'rest_framework',
    # IMPORTANT: We're adding adding a new app here to
    # enable token-based authentication. Don't forget to
    # apply the database migrations!
    'rest_framework.authtoken',
    # ...the local app goes here
]

# ...other settings go here

REST_FRAMEWORK = {
    # Rendering
    # https://www.django-rest-framework.org/api-guide/renderers/
    'DEFAULT_RENDERER_CLASSES': [
        'rest_framework.renderers.JSONRenderer',
    ],

    # Authentication
    # https://www.django-rest-framework.org/api-guide/authentication/
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.TokenAuthentication',
    ],

    # Permissions
    # https://www.django-rest-framework.org/api-guide/permissions/
    'DEFAULT_PERMISSION_CLASSES': [
        # 'rest_framework.permissions.IsAuthenticated',
    ],
}

In the snippet above, we declared the following policies for our API:

  • Our API will only respond in JSON format (see the renderer classes).
  • Users must be authenticated to make a request (see the permission classes).
  • We will use tokens for user authentication (see the authentication classes).

The policies you specify in settings.py will be applied to all of your APIViews. This is very convenient and helps keep our code DRY.

If you try to make either a GET or a POST request from the examples provided earlier, you will see the following response:

{"detail":"Authentication credentials were not provided."}

Furthermore, if you try to open the endpoint URL in your browser, you’ll notice that the Browsable API is gone. This is because the Django REST Framework includes it in the default settings, which we now overwrote.

While the Browsable API might be handy during development, it might be undesired in production, particularly because it relies on SessionAuthentication, which violates one of the REST principles – statelessness, and forces you to have an additional authentication method.

You can add the following snippet to your settings.py to only allow the Browsable API and SessionAuthentication in development mode:

# ...REST_FRAMEWORK settings go here


# The following will only be added in the debug mode
if DEBUG:
    # Rendering
    REST_FRAMEWORK['DEFAULT_RENDERER_CLASSES'].append(
        'rest_framework.renderers.BrowsableAPIRenderer',
    )
    # Authentication
    REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'].append(
        'rest_framework.authentication.SessionAuthentication',
    )
NOTE: If you're building an API for an existing Django app with server-rendered HTML pages, then you're likely going to make AJAX calls to your API from your app's pages. In this case, it makes perfect sense to use SessionAuthentication.

Our tutorial project assumes that we are building a standalone API, which are typically consumed by mobile apps and single-page applications (SPA).

Setting policies on individual APIViews

In some cases, you might want an endpoint to have its own behaviour. For example, a login endpoint has to allow unauthenticated requests – otherwise, the users won’t be able to log in!

Luckily, DRF allows us to easily override global policies on a single endpoint. You can do so by declaring special properties under your view class. They will take priority over those specified in settings.py.

The following example won’t go into our tutorial project; it is here to demonstrate how individual policies are declared:

# First, we have to import the desired modules containing
# the classes of API policies. In a real use case you're 
# unlikely to need all of them at once, but rather one or 
# two at a time.
from rest_framework import (
    authentication, 
    permissions, 
    parsers,
    renderers,
)


class CustomPolicyAPIView(APIView):
    authentication_classes = [authentication.BasicAuthentication]
    permission_classes = [permissions.AllowAny]
    parser_classes = [parsers.JSONParser]
    renderer_classes = [renderers.JSONRenderer]

Note that these properties expect an iterable object (such as list or tuple) as a value. If you wish to strip all policies on any of these properties, you can set it as an empty list ([]).

Function-based views in DRF

Now let’s look at another way of declaring views in the Django REST Framework – as functions. These are particularly useful for short, simple endpoints.

We can convert a regular python function into a view by using a special decorator @api_view on it. Here’s how we could rewrite our random number view class using a function-based declaration:

# 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: we 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 @api_view decorator generates an anonymous instance of the APIView class. Then it sets our function as its default request handler. Thus, function-based views are nothing more than a shortcut for creating class-based views.

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. Here is what the urls.py would look like:

from django.urls import path
# NOTE: Import the view function here
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'
    ),
]

Handling various request methods in Function-based views

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 Method Not Allowed error.

We can pass a list of strings representing the methods we’d like to accept to the @api_veiew decorator call. Here is a snippet showing how we can make our view accept POST requests:

# ...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')

    # If it's not POST, then it must be GET, since others return 405
    else:
        # We could do this even simpler by providing a default
        # value upon getting request data, i.e:
        #   request.data.get('min', 1)
        # But we stick to if...else to demonstrate `request.method` property
        min_num = 1
        max_num = 10

    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 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 to Function-based views

Lastly, you can configure the behaviour of your function-based view just as easily as you can with the class-based views. Instead of class properties, we use special 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, 
    renderer_classes,
)
# Import the modules containing classes of API policies
from rest_framework import (
    authentication, 
    permissions, 
    parsers,
    renderers,
)

@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])
@renderer_classes([renderers.JSONRenderer])
def get_random_number(request):
    # ... Function logic goes here

Just like with class-based views, you can pass an empty list ([]) as a value to any of those decorators to strip all policies.

When to use APIView and Function-based views?

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

As such, the APIView class defines foundational logic for building API endpoints, but it lacks any functionality for working with database models. And since the function-based views are essentially the same as APIView this applies to them too.

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 third part of our comprehensive series 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 APIView might be useful are:

  • Login, logout, and 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)

Function-based views can be used in the same scenarios, particularly when the logic of your endpoint is fairly simple.

Conclusion

We’ve reviewed what can probably be called the most fundamental class of the Django REST Framework – the APIView class.

We’ve looked at some code samples showing how to use it and discussed that it generally makes sense to use it when not operating on database models.

We’ve also touched upon function-based views created with the @api_view decorator, which is a handy shortcut allowing you to save a few lines of code and thus have better readability.

The following parts of this series will cover tools for working with form data and database models, such as Serializer and ModelSerializer. We will also look into some subclasses of APIView used for working with database models, such as GenericAPIView and ViewSet.

Leave a Reply

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

You May Also Like