Django REST Framework Tutorial: Todo API
Updated
Table of Contents
Django REST Framework is a powerful library that sits on top of existing Django projects to add robust web APIs. If you have an existing Django project with only models and a database--no views, URLs, or templates required--you can quickly transform it into a RESTful API with a minimal amount of code.
In this tutorial, we'll create a basic Django To Do app and then convert it into a web API using serializers, viewsets, and routers.
Prerequisites
You should have a basic understanding of how Django works and a local development configuration with Python. I've written an entire book called Django for Beginners that introduces these topics and has a dedicated chapter on local dev setup.
Initial Set Up
Let's start by creating a directory for our code and a new virtual environment.
# Windows
$ cd onedrive\desktop\drftodo
$ mkdir drf_todo
$ cd drf_todo
$ python -m venv .venv
$ .venv\Scripts\Activate.ps1
(.venv) $
# macOS
$ cd ~/desktop/drftodo
$ mkdir drf_todo
$ cd drf_todo
$ python3 -m venv .venv
$ source .venv/bin/activate
(.venv) $
Then install Django, create a new project called django_project
, and a new app called todos
.
(.venv) $ python -m pip install django
(.venv) $ django-admin startproject django_project .
(to.venvdo) $ python manage.py startapp todos
Since we have a new app, we need to update our INSTALLED_APPS
setting. Open django_project/settings.py
with your text editor and add todos
at the bottom of the list.
# django_project/settings.py
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"todos", # new
]
Perform our first migration to set up the initial database at this point.
(.venv) $ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying auth.0012_alter_user_first_name_max_length... OK
Applying sessions.0001_initial... OK
Next, we can create a basic model for our Todo
app which will just have a title
, description
, and add a __str__
method.
# todos/models.py
from django.db import models
class Todo(models.Model):
title = models.CharField(max_length=200)
description = models.TextField()
def __str__(self):
return self.title
Then create a migration file.
(.venv) $ python manage.py makemigrations todos
Migrations for 'todos':
todos/migrations/0001_initial.py
- Create model Todo
And migrate our change to the project database.
(.venv) $ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions, todos
Running migrations:
Applying todos.0001_initial... OK
The final step is to update the admin.py
file so the todos
app is visible when we log into the admin.
# todos/admin.py
from django.contrib import admin
from .models import Todo
admin.site.register(Todo)
In a normal Django app at this point we would need to add URLs, views, and templates to display our content. But since our end goal is an API that will transmit the data over the internet, we don't actually need anything beyond our models.py
file!
Let's create a superuser account so we can log into the admin and add some data to our model. Follow the prompts to add a name, email, and password. If you're curious, I've used admin
, [email protected]
, and testpass123
since this example is for learning purposes. Obviously use a name and password that are more secure on a real site!
(.venv) $ python manage.py createsuperuser
(.venv) $ python manage.py runserver
Navigate to http://127.0.0.1:8000/admin
and log in with your new superuser account.
On the Admin homepage, you should see the todos
app. Click on "+ Add" button for Todos and add two new entries we can use. I've called mine the following:
- title: 1st todo; description: Learn DRF
- title: 2nd item; description: Learn Python too.
Now it's time for Django REST Framework.
Django REST Framework
The first step is to install Django REST Framework and then create a new apis
app. All of our API information will be routed through here. Even if we had multiple apps in our project, we'd still only need a single apis
app to control what the API does.
On the command line, stop the server with Control+c
and then enter the following.
(.venv) $ python -m pip install djangorestframework~=3.15.2
(.venv) $ python manage.py startapp apis
Next, we add both the rest_framework
and the apis
app to our INSTALLED_APPS
setting. We also add default permissions. In the real world, we would set various permissions here so that only logged-in users could access the API, but for now, we'll just open up the API to everyone to keep things simple. The API is just running locally, after all, so there's no real security risk.
# django_project/settings.py
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework", # new
"apis", # new
"todos", # new
]
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.AllowAny",
]
}
A traditional Django app needs a dedicated URL, view, and template to translate information from that database onto a webpage. In DRF, we instead need a URL, view, and serializer. The url controls access to the API endpoints, views control the logic of the data being sent, and the serializer performs the magic of converting our information into a format suitable for transmission over the internet, JSON.
If you're new to APIs then serializers are probably the most confusing part of the equation. A normal webpage requires HTML, CSS, and JavaScript (usually). But our API is only sending data in the JSON format. No HTML. No CSS. Just data. The serializer translates our Django models into JSON and then the client app translates JSON into a full-blown webpage. The reverse, deserialization, also occurs when our API accepts a user input--for example, submitting a new todo--which is translated from HTML into JSON and then converted into our Django model.
So, to repeat one last time, URLs control access, views control logic, and serializers transform data into something we can send over the Internet.
Within our apis
app we need to create a apis/serializers.py
file. Much of the magic comes from the serializers
class within DRF, which we'll import at the top. We need to import our desired model and specify which fields we want to be exposed (usually, you don't want to expose everything in your model to the public).
# apis/serializers.py
from rest_framework import serializers
from todos import models
class TodoSerializer(serializers.ModelSerializer):
class Meta:
fields = (
"id",
"title",
"description",
)
model = models.Todo
Next up is our view. DRF views are very similar to traditional Django views and even come with several generic views that provide lots of functionality with a minimal amount of code on our part. We want a ListView as well as a DetailView of individual todo items. Here's what the code looks like.
# apis/views.py
from rest_framework import generics
from todos import models
from .serializers import TodoSerializer
class ListTodo(generics.ListCreateAPIView):
queryset = models.Todo.objects.all()
serializer_class = TodoSerializer
class DetailTodo(generics.RetrieveUpdateDestroyAPIView):
queryset = models.Todo.objects.all()
serializer_class = TodoSerializer
Again DRF performs all the heavy lifting for us within its generics
class that we import at the top. This is quite similar to generic class-based views in traditional Django. We specify our model and serializer for each of the views.
All that's left is to update our URLs. At the project level, we want to include the apis
app. So we'll add include
to the second line imports and then add a dedicated URL path for it. Note that the format is apis/v1/
. It's a best practice to always version your APIs since they are likely to change in the future but existing users might not be able to update as quickly. Therefore a major change might be at apis/v2/
to support both versions for a period of time.
# django_project/urls.py
from django.contrib import admin
from django.urls import include, path # new
urlpatterns = [
path("admin/", admin.site.urls),
path("apis/v1/", include("apis.urls")), # new
]
Finally, we need to add a new apis/urls.py
file to display our views. The list of all todos will be at apis/v1/
. Individual todo items will be at their pk
which is automatically set by Django for us. So the first todo will be at apis/v1/1/
, the second at apis/v1/2/
, and so on.
# apis/urls.py
from django.urls import path
from .views import ListTodo, DetailTodo
urlpatterns = [
path("", ListTodo.as_view()),
path("<int:pk>/", DetailTodo.as_view()),
]
Ok, we're done! That's it. We now have an API of our To do project. Go ahead and start the local server with runserver
.
(.venv) $ python manage.py runserver
Testing with the Web Browser
DRF comes with a very nice graphical interface for our API, similar in some ways to Django's admin
app. If you simply go to an API endpoint you can see it visualized.
The list view of all items is at http://127.0.0.1:8000/apis/v1/
.
And the DetailView is at http://127.0.0.1:8000/apis/v1/1/
.
You can even use the forms on the bottom of each page to create, retrieve, destroy, and update new todo items. When your APIs become even more complex many developers like to use Postman to explore and test an API. But the usage of Postman is beyond the scope of this tutorial.
Viewsets
As you build more and more APIs you'll start to see the same patterns over and over again. Most API endpoints are some combination of common CRUD (Create-Read-Update-Delete) functionality. Instead of writing these views one-by-one in our views.py
file as well as providing individual routes for each in our urls.py
file we can instead use a ViewSet which abstracts away much of this work.
For example, we can replace our two views and two url routes with one viewset and one URL route. That sounds better, right? Here's what the new code for views.py
looks like with a viewset.
# apis/views.py
from rest_framework import viewsets
from todos import models
from .serializers import TodoSerializer
class TodoViewSet(viewsets.ModelViewSet):
queryset = models.Todo.objects.all()
serializer_class = TodoSerializer
The viewsets
class does all the magic here, specifically the method ModelViewSet
which automatically provides list as well as create, retrieve, update, and destroy actions for us.
We can update our urls.py
file to be much simpler too as follows.
# apis/urls.py
from django.urls import path
from .views import TodoViewSet
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register("", TodoViewSet, basename="todos")
urlpatterns = router.urls
Now if you look again at our pages you'll see that the list view at http://127.0.0.1:8000/apis/v1/
is exactly the same as before. And our detailview at http://127.0.0.1:8000/apis/v1/1/
has the same functionality--the same HTTP verbs allowed--though now it is called a "Todo Instance".
This saving of code may seem small and not worth the hassle in this simple example, but as an API grows in says with many, many API endpoints it often is the case that using viewsets and routers saves a lot of development time and makes it easier to reason about the underlying code.
Next Steps
This is a working, albeit basic implementation of a Todo API. Additional features might include user authentication, permissions, and more which are covered in my book Django for APIs. The first three chapters are available to read for free.