Your first Django REST API

Your first Django REST API

Requirements

  • Basic knowledge of Python

  • Basic understanding of Django framework

REST API

Rest API is a term concatenated from 2 abbreviations.

  • REST stands for REpresentational State Transfer

  • API stands for Application Programming Interface

REST is an architectural style for distributed hypermedia systems that abide by some rules.

  • Client and server sides are separated.

  • Each request from the client to the server must contain all of the information (it is stateless).

  • Interface is uniformed (all the API points should be accessible by a similar approach).

API is a specification by some software that allows other programs to interact with it. It allows a developer to make a specific request to send or receive information.

So, REST API is a web service for programs that uses REST architectural style and abides by its rules.

Django REST Framework

Django REST Framework is a powerful tool for creating REST APIs based on Django. It comes with built-in serialization, authentication and it's highly customizable. Besides that, APIs built with it are browsable out of the box. That means that you're able to use it almost like a simple web application. You can add data and view it in a browser.

Another great thing is that it comes with great documentation.

Squats, pushups, and API

A bunch of programmer friends decided they want to do more for their bodies. They plan to do as many pushups and squats as they can each day and compete against each other. The person that does the most of both at the end of the period, gets a reward.

Since they're programmers, each of them will create their own client and analyze data in their own way. To be able to see where the competitors are, they need an API. They'll send their daily record to the API and will be able to retrieve data about others back.

Each day, they'll post how many pushups and squats they did, but they don't need daily records of others back. They just need to know the total count of both for every other competitor.

Initial setup

Start with creating a project and virtual environment:

$ mkdir health_challenge
$ python3.9 -m venv env
$ source env/bin/activate

Next, install Django and Django Rest Framework:

(env)$ pip install django
(env)$ pip install djangorestframework

After that, create a new Django project and app:

(env)$ django-admin.py startproject tutorial .
(env)$ django-admin.py startapp challenge

adding a dot after the name of the project prevents Django from creating a redundant parent directory

Once the project is created, register Django Rest Framework (DRF) and the challenge app to installed apps inside "tutorial/settings.py" :

# tutorial/settings.py
INSTALLED_APPS = [
    # ...
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'challenge',
]

Models

Create a Record model inside the "challenge/models.py":

# challenge/models.py
from django.utils import timezone
from django.db import models

class Record(models.Model):
    date = models.DateField(default=timezone.now)
    pushups = models.PositiveIntegerField(default=0)
    squats = models.PositiveIntegerField(default=0)

    def __str__(self):
        return self.date

For now, it has 3 attributes: date, pushups, and squats. You'll add the creator later.

Create an initial migration and apply it:

(env)$ python manage.py makemigrations
(env)$ python manage.py migrate

Register the Record model in challenge/admin.py so you can access it from the Django admin panel:

# challenge/admin.py
from django.contrib import admin
from challenge.models import Record

admin.site.register(Record)

If you want to access the admin panel to check your work, you have to create a superuser:

(env)$ python manage.py createsuperuser

Enter the required data (remember the password!).

Now, run the local server for the very first time:

(env)$ python manage.py runserver

Navigate to the admin panel in your application: 127.0.0.1:8000/admin and open Records. Add 2 new records.

Serializers

Instead of a page composed of HTML, CSS, and JS, an API only provides raw data. That data needs to be in some kind of form that it's easy for a program to extract it (JSON/XML). In DRF, Serializer takes care of that. DRF Serializer covers serialization and deserialization of data and its validation. There are two possible classes to use when you need a serializer. Serializer class is a powerful generic way to control the output, ModelSerializer is a specialized serializer for model instances. Considering you're working with your models, you'll use the second one.

Create a new file serializers.py inside challenge directory:

# challenge/serializers.py
from rest_framework import serializers
from .models import Record


class RecordSerializer(serializers.ModelSerializer):
    class Meta:
        model = Record
        fields = ("id", "date", "pushups", "squats")

ModelSerializerclass creates a Serializer with a set of fields that corresponds to the selected Model. As you can see, you had to import the serializer and the Record model.

Inside the ModelSerializer there's a class Meta in which you specify which model you want to use and which fields from the Model to serialize.

Even if you want to serialize all the data, it is recommended to specify each field explicitly. That saves you from exposing too much data by accident if you change your Model. Ignoring that, you can specify all the fields with fields = "__all__".

View

Since you're not building a typical website, you won't use a classical Django View. DRF provides a View subclass - APIView.

GenericAPIView which extends APIView, provides concrete generic views that cover commonly required behaviors for lists and detail views.

There are quite a few concrete generic views you could use:

  • CreateAPIView is used for resource creation endpoints

  • ListCreateAPIView is used for a read-write list of resources

  • RetrieveUpdateDestroyAPIView is used for read-write-delete on single resource

  • ...

You can check all of them here

Since you're creating an endpoint for posting the records, you'll use CreateAPIView generic view. Create a new file view.py:

from rest_framework import generics
from .serializers import RecordSerializer


class RecordAdd(generics.CreateAPIView):
    serializer_class = RecordSerializer

There is no data to retrieve, so you just need to set the serializer_class to the previously created RecordSerializer.

URL

The last thing to do is to add an url where you can access your view. Create an urls.py inside the challenge folder.

# challenge/urls.py
from django.urls import path
from .views import RecordAdd

urlpatterns = [
    path('add-record', RecordAdd.as_view())
]

After that, include that file inside "tutorial/urls.py":

#tutorial/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path("", include("challenge.urls")),
]

Now you can navigate to 127.0.0.1:8000/add-record and voila:

record_add_view.png

Your first API endpoint!

Add a record and go check it in the admin panel: 127.0.0.1:8000/admin/challenge/record. You should see the record you entered via API!

Record owners

If your app is about the competition between record creators, you need to be able to assign created records to a user. That's how you'll know who made that many pushups.

Start by changing a Record model. You need to add a creator field to it.

# challenge/models.py
# ...

class Record(models.Model):
    date = models.DateField(default=timezone.now)
    pushups = models.PositiveIntegerField(default=0)
    squats = models.PositiveIntegerField(default=0)
    creator = models.ForeignKey('auth.User', on_delete=models.CASCADE)

# ...

Stop the local server with Ctrl+C and run (env)$ python manage.py makemigrations. Since you've already created some records it won't go as easy as the first time.

You'll get asked the following:

You are trying to add a non-nullable field 'creator' to record without a default; you can't do that (the database needs something to populate existing rows).
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Quit, and let me add a default in models.py

Select 1 and type 1 in again, so your superuser will be the creator of current records. Sync your database with (env)$ python manage.py migrate.

Make some minor changes, so you can check if your Model change works as it should.

First, create another superuser: shell (env)$ python manage.py createsuperuser Next, add a login to the browsable API. Include rest_framework.urls in the "tutorial/urls.py":

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
  path('admin/', admin.site.urls),
  path('api-auth/', include('rest_framework.urls')),
  path("", include("challenge.urls")),
]

If you refresh the page (127.0.0.1:8000/add-record), you'll see a little arrow next to your username on the right side of the page:

login.png

Click on it and you can logout and login with the other superuser you created.

After that, change the list display for records admin. Add a RecordAdmin with the list_display specified to the "challenge/admin.py":

# challenge/admin.py
from django.contrib import admin
from challenge.models import Record

class RecordAdmin(admin.ModelAdmin):
    list_display = ('date', 'creator')

admin.site.register(Record, RecordAdmin)

This is not necessary, solely intent of this is that you can easily see who's the records creator.

Now, logged in as another user, try to create a new record via browsable API and... Ups, you've got an error.

NOT NULL constraint failed: challenge_record.creator_id

That's because now you demand a creator in your model, but you didn't provide it. You don't want to add it via form, as you did it for other data. You want for the creator to be automatically added, based on which user posted the new record. You can hook on the objects save method in your view and overwrite it, so creator = user.

# challenge/views.py
# imports here

class RecordAdd(generics.CreateAPIView):
    serializer_class = RecordSerializer

    def perform_create(self, serializer):
        serializer.save(creator=self.request.user)

Try to create a record, logged in as another user again and you should see no error. Now, go check the records admin panel at 127.0.0.1:8000/admin/challenge/record and you can see that you created another record with a different user.

list_records_admin.png

List all users with their records

You covered the posting-the-record part. Now move to the second part of your requirements - getting the data for all the competitors. Before you proceed and make things complicated, start simple.

First, add a serializer CompareSerializer inside "challenge/serializers.py*:

# challenge/serializers.py
# imports and RecordSerializer here

class CompareSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ("__all__")

You retrieve all the data that you have on each user.

Next, add a new view inside "challenge/views.py":

# challenge/views.py
# imports and RecordAdd here
from .serializers import RecordSerializer, CompareSerializer
from django.contrib.auth.models import User


class CompareView(generics.ListAPIView):
    queryset = User.objects.all()
    serializer_class = CompareSerializer

There are some differences with the RecordAdd view:

  1. Instead of CreateAPIView, you use ListAPIView - that's because you don't want to add a new record but to see all the records.

  2. You didn't need the queryset for RecordAdd, because you didn't do anything with previous records. Now you're showing all the records, so you need to retrieve them.

  3. You used the CompareSerializer instead of RecordSerializer

After that, add an url to "challenge/urls.py":

# challenge/urls.py
# imports here

urlpatterns = [
    path('add-record', RecordAdd.as_view()),
    path('compare', CompareView.as_view())
]

Now navigate to 127.0.0.1:8000/compare and ... - ok, it's working but it's not the data you need.

SerializerMethodFields

SerializerMethodField is a read-only field that gets its value by calling a method on its Serializer. You can use it to add any sort of data to your serialized object.

Edit your CompareSerializer:

# challenge/serializers.py
from django.db.models import Sum
# other imports and RecordSerializer

class CompareSerializer(serializers.ModelSerializer):
    pushups = serializers.SerializerMethodField()

    class Meta:
        model = User
        fields = ("username", "pushups")

    def get_pushups(self, obj):
        return obj.records.aggregate(Sum('pushups'))['pushups__sum']

SerializerMethodField and its method are automatically connected with a DRF method get_<field_name>. That means that the belonging method has the same name as the field, just prefixed with get_. You need to add the SerializerMethodField to the Serializers fields.

Now for the method: obj refers to the instance of Model you're using for your Serializer. .aggregate is a Django's function that summarizes a collection of objects and with Sum, you calculate their sum. obj.records.aggregate(Sum('pushups'))returns a dictionary, so you add ['pushups__sum'] to access just the desired key.

Refresh 127.0.0.1:8000/compare and check if it works.

pushups_sum.png

You got the count of pushups! To provide all the data you wanted, you just need to do the same for the squats.

class CompareSerializer(serializers.ModelSerializer):
    pushups = serializers.SerializerMethodField()
    squats = serializers.SerializerMethodField()

    class Meta:
        model = User
        fields = ("username", "pushups", "squats")

    def get_pushups(self, obj):
        return obj.records.aggregate(Sum('pushups'))['pushups__sum']

    def get_squats(self, obj):
        return obj.records.aggregate(Sum('squats'))['squats__sum']

You added another SerializerMethodFIeld and its method. Don't forget to add this method field to the list of fields inside the Meta class. Refresh the page and the second APIView is completed!

pushups_squats_count.png

Limit the API solely to authenticated users

You don't want the data of your users to be seen by anyone who just wanders around your API. You want to limit the access to this API point. There are 3 options for limiting the access:

  • Project-level (restrict the access to the whole API)

  • View-level (restrict the access to whole API View)

  • Object-level (restrict the access only for part of the view)

Default permission policy

Currently, you don't want someone that is not registered, to see anything. You'll restrict access to your API globally. Edit "tutorial/setting.py" and add this at the bottom:

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ]
}

With this, you allow access to your API only to authenticated users. Logout from the API and you'll see this:

access_restricted.png

Even if you navigate to /add-record, the message you will be the same.

Remember how to do that, but for now, delete the code you added in the settings.py file (REST_FRAMEWORK ... IsAuthenticated',]}). You need to learn about another possibility.

View level permission

Let's say the family and friends of the competitors want to monitor the competition. You want them to see the list of competitors, but you don't want them to add the records. If they'd try, they'd get a nasty error:

not_authenticated_error.png

You don't want that. You want to limit the access for the add-record endpoint.

Edit the RecordAdd class in "challenge/views.py":

# challenge/views.py

from rest_framework.permissions import IsAuthenticated # don't forget the import!

class RecordAdd(generics.CreateAPIView):
    permission_classes = [IsAuthenticated]

    serializer_class = RecordSerializer

    # ...

Open 127.0.0.1:8000/add-record. You're logged out, so this is the view you get to see:

add_restricted.png

Now navigate to 127.0.0.1:8000/compare. Even when you're logged out, your view is the same as for an authenticated user:

compare_open.png

If you log in and go back to /add-record, you'll be able to post a new record:

logged_in.png

Check all the possibilities of the restrictions here.

Root API Endpoint

You can make your API easier to browse. On your root page(/), you want to make a list of all the endpoints.

For that, you need to create another view and connect it with the root url.

For this, you don't need Serializer, you'll use just a simple Django function-based view.

Add this at the top of the "challenge/views.py"e:

# challenge/views.py
# ... imports here
@api_view()
def api_root(request, format=None):
    return Response({
        'add': reverse('add-record', request=request, format=format),
        'compare': reverse('compare', request=request, format=format)
    })

# ... RecordAdd and CompareView here

Django receives an instance of HttpRequest and returns HttpResponse, but DRF receives an instance of Request and returns Response. To change a Django view to a DRF view, use the @api_view decorator. You provide both API points in the Response. reverse returns a fully qualified URL, using the request to determine the host and port. That way, the urls are correct, either on your localhost or in the production, without the need to change anything in the code.

add-record inside the reverse method corresponds to the name of the url. You don't have that in your "challenge/urls.py". Add it:

# challenge/urls.py

from django.urls import path

from .views import RecordAdd, CreatorList, CompareView, api_root

urlpatterns = [
    path('add-record', RecordAdd.as_view(), name='add-record'),
    path('compare', CompareView.as_view(), name='compare'),
    path('', api_root)
]

You imported the api_root view you wrote a few minutes back and added it to the root of your website. To the paths add-record and compare you added names, so the reverse function in api_root can find them.

Open http://127.0.0.1:8000/ and check it yourself - it works!

api_root.png

You can also click on links of add and compare and that makes it easy to navigate through your API.

Conclusion

You're now able to create a REST API with Django Rest Framework. You know how to create views that either allow you to post or to retrieve data. You know how to create serializers that change your Model data to JSON. You learned how to restrict access and how to make your API browsable. You also learned how to change the data you get from the Models and serve the change data on your API.

You can play around with it a little more:

  • create an endpoint that will output the current winner

create an endpoint where only an owner will be able to see and change/delete their record

If you're looking for more advanced content, you can check the articles on DRF that I wrote on Testdriven.io:
- Django REST Framework Views series
- Permissions in Django REST Framework series

Image by slightly_different on Pixabay

Did you find this article valuable?

Support GirlThatLovesToCode by becoming a sponsor. Any amount is appreciated!