Table of contents
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")
ModelSerializer
class 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 withfields = "__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:
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:
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 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:
Instead of
CreateAPIView
, you useListAPIView
- that's because you don't want to add a new record but to see all the records.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.You used the
CompareSerializer
instead ofRecordSerializer
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 Serializer
s 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.
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!
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:
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:
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:
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:
If you log in and go back to /add-record, you'll be able to post a new record:
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!
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