How to create an API with Django

Illustration of a stylized person wearing sunglasses and a scarf lightly sprinkling seasoning onto a small object on a plate. Illustration of a stylized person wearing sunglasses and a scarf lightly sprinkling seasoning onto a small object on a plate.

An API, or Application Programming Interface, is a broadly defined area of backend development. Essentially, the API we will examine today is a website without the frontend: instead of rendering HTML pages, this backend returns data in the JSON format for use in native or web applications. Just like when creating a website, the most important thing to consider when writing an API is how it will be used. Today, we will discover how to use Django to create an API for a basic ToDo application.

Following this tutorial will require a few tools. I encourage you to follow along with this article by cloning the example project from this GitHub repository. You should have Python 3, Django 2.2, and djangorestframework 3.9 installed (from the repository, run pip install -r requirements.txt to install the libraries). For guidance on installing Django, please consult the official documentation. Also, you'll want to download the free version of Postman. Postman is an excellent tool for developing and testing APIs, and we will only scratch the surface of its features in this article.

To start, navigate to the taskmanager directory that contains manage.py and run the command python manage.py migrate to apply the database migrations to Django's default sqlite database. Make a superuser with python manage.py createsuperuser, be sure to record the username and password as we will need them later. Then, run python manage.py runserver to interact with the API.

There are two ways to interact with the API: browsing the Django REST framework frontend and making http requests. Open your web browser and navigate to 127.0.0.1:8000, or localhost at port 8000, where Django projects run by default. You should see a webpage showing a list of available API endpoints. This is an important principle in the RESTful approach to API development: the API itself should show users what is available and how to use it.

First, look in views.py at the api_index function. This function provides the list of endpoints that you are visiting.

    @define_usage(returns={'url_usage': 'Dict'})
    @api_view(['GET'])
    @permission_classes((AllowAny,))
    def api_index(request):
        details = {}
        for item in list(globals().items()):
            if item[0][0:4] == 'api_':
                if hasattr(item[1], 'usage'):
                    details[reverse(item[1].__name__)] = item[1].usage
        return Response(details)

There are three decorators on each API view. We'll come back to @define_usage in a second. @api_view comes from the Django REST framwork and provides two things: the template for the webpage you're looking at, and the HTTP method that the endpoint supports. @permission_classes, also from the Django REST framwork, uses AllowAny to permit anyone to access this url without any authentication. The body of the function looks at the global scope within the app to collect every defined function. As we prepended every api view function name with api_, we can filter those out and return a dictionary of their usage information. Said usage details come from a custom decorator defined in decorators.py.

    def define_usage(params=None, returns=None):
        def decorator(function):
            cls = function.view_class
            header = None
            # Is authentication required to access this view?
            if IsAuthenticated in cls.permission_classes:
                header = {'Authorization': 'Token String'}
            # Build a list of the valid methods, but take out 'OPTIONS'
            methods = [method.upper() for method in cls.http_method_names if method != 'options']
            # Build response dictionary
            usage = {'Request Types': methods, 'Headers': header, 'Body': params, 'Returns': returns}


            # Prevent side effects
            @wraps(function)
            def _wrapper(*args, **kwargs):
                return function(*args, **kwargs)
            _wrapper.usage = usage
            return _wrapper
        return decorator

A decorator is a piece of syntax that allows us to easily define higher-order functions to give the view functions attributes like a class. Function-based views with decorators are a great middle ground between plain functions and class-based views in Django. This decorator provides four pieces of information: the request types, headers, parameters, and return values of each function. The decorator generates the header and method information from the other decorators attached to each function and takes the parameters and return values defined at its invokation on each view function as input.

Other than the index, we will be using the API for interacting with user data, so we will want some sort of authentication. If this were more than a sample project, we would implement a sign up function (which is a great exercise if you want to check your understanding of the concepts in this article). Instead, we will simply sign in as the superuser that we created earlier.

 @define_usage(params={'username': 'String', 'password': 'String'},
               returns={'authenticated': 'Bool', 'token': 'Token String'})
 @api_view(['POST'])
 @permission_classes((AllowAny,))
 def api_signin(request):
     try:
         username = request.data['username']
         password = request.data['password']
     except:
         return Response({'error': 'Please provide correct username and password'},
                         status=HTTP_400_BAD_REQUEST)
     user = authenticate(username=username, password=password)
     if user is not None:
         token, _ = Token.objects.get_or_create(user=user)
         return Response({'authenticated': True, 'token': "Token " + token.key})
     else:
         return Response({'authenticated': False, 'token': None})

It's important to note that you need to configure a variety of settings to make token-based authentication work. You can see a complete configuration in the settings.py file of the example project.

The api_signin method takes a username and password and uses Django's built-in authenticate method to verify the user. If the credentials provided are correct, it will return a token that allows the client to access the restricted endpoints of the API. Note that the token should be stored securely in the client application as it has the same access power as a password. Now is a great time to switch over to Postman. Open the application and use it to send a post request to /signin/ as shown in the following screenshot.

Now that you have a token for your user, you can explore the rest of the API. Let's take a quick break to examine the model behind the API.

    class Task(models.Model):
        user = models.ForeignKey(User, on_delete=models.CASCADE) #Each task is owned by one user
        description = models.CharField(max_length=150) #Each task has a description of what needs to be done
        due = models.DateField() #Each task has a due date, which is a Python datetime.date

The Task model is a fairly simple approach to ToDo management. Each item has a description, like "Write a Django API" and a due date. Tasks also have foreign key relationship with the Django user object, that is, each Task is associated with exactly one User, but each User may have any number of Tasks, even zero. Implicitly, every Django object also has an id, a unique integer that we can use to reference individual Tasks.

An application like this is often called a CRUD application, which stands for "Create, Read, Update, Destroy." These are the four operations that our application supports on Task objects.

Let's start by getting the empty list of tasks currently associated with the user. Use Postman to make a GET request to the /all/ url, as shown in the screenshot below. Remember to add your token to the headers of this and all future requests.

    @define_usage(returns={'tasks': 'Dict'})
    @api_view(['GET'])
    @authentication_classes((SessionAuthentication, BasicAuthentication, TokenAuthentication))
    @permission_classes((IsAuthenticated,))
    def api_all_tasks(request):
        tasks = taskSerializer(request.user.task_set.all(), many=True)
        return Response({'tasks': tasks.data})

The api_all_tasks function is fairly straightforward. One thing to note is the change of authentication requirements and permission classes to require token authentication. We have a new decorator, @authentication_classes, which allows the default Django REST framework authentication methods as well as TokenAuthentication. This allows us to reference user instances as request.user, exactly as if they had logged in with a standard Django session. The @define_usage decorator show us that api_all_tasks takes no parameters (as it is a GET request) and returns only one thing: a serialized list of tasks. As this function returns data in the JSON (JavaScript Object Notation) format, we use a serializer to tell Django how to parse the data for sending.

    class taskSerializer(serializers.ModelSerializer):
        class Meta:
            model = Task
            fields = ('id', 'description', 'due')

This simple model defines the data for a Task as its id, description, and due date. Serializers can add and exclude fields and data from a model, for example, this one does not return the user id as it is not useful for our end client.

    @define_usage(params={'description': 'String', 'due_in': 'Int'},
                  returns={'done': 'Bool'})
    @api_view(['PUT'])
    @authentication_classes((SessionAuthentication, BasicAuthentication, TokenAuthentication))
    @permission_classes((IsAuthenticated,))
    def api_new_task(request):
        task = Task(user=request.user,
                    description=request.data['description'],
                    due=date.today() + timedelta(days=int(request.data['due_in'])))
        task.save()
        return Response({'done': True})

Now, we want to create a task, so we use api_new_task. In general, to create an object in a database, we use a PUT request. Note that this, and both other methods, do not require any serialization. Instead, here we will pass the parameters into the initialization of a Task object, which we then save to the database. For simplicity, we send the number of days a task is due in rather than attempting to send a Python Date object, then in the API we save a date that many days in the future. Make a request to /new/ to create a task then run the /all/ request again to see your created object.

    @define_usage(params={'task_id': 'Int', 'description': 'String', 'due_in': 'Int'},
                  returns={'done': 'Bool'})
    @api_view(['POST'])
    @authentication_classes((SessionAuthentication, BasicAuthentication, TokenAuthentication))
    @permission_classes((IsAuthenticated,))
    def api_update_task(request):
        task = request.user.task_set.get(id=int(request.data['task_id']))
        try:
            task.description = request.data['description']
        except: #Description update is optional
            pass
        try:
            task.due = date.today() + timedelta(days=int(request.data['due_in']))
        except: #Due date update is optional
            pass
        task.save()
        return Response({'done': True})

To edit this task we just created, we make a POST request to api_update_task at /update/. We include the task_id to reference the correct task from the user's task_set. The code here is a touch more complex, as we want to allow for the possibility of updating the description and/or the due date.

    @define_usage(params={'task_id': 'Int'},
                  returns={'done': 'Bool'})
    @api_view(['DELETE'])
    @authentication_classes((SessionAuthentication, BasicAuthentication, TokenAuthentication))
    @permission_classes((IsAuthenticated,))
    def api_delete_task(request):
        task = request.user.task_set.get(id=int(request.data['task_id']))
        task.delete()
        return Response({'done': True})

To delete a task, use a DELETE request to api_delete_task at /delete/. This method works identically to the api_update_task function, except that rather than modify the identified task it deletes it.

Now we have seen how to implement an index, token-based authentication, and the four major HTTP methods for a Django API. You can use these concepts to support all kinds of web and native mobile applications or to develop a public data sharing API. Feel free to clone the example project, which contains all of the code for this article, and try to implement extensions like pagination, rate limiting, and user creation.