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.

Admin homepage

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.

Admin todos

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/.

API ListView

And the DetailView is at http://127.0.0.1:8000/apis/v1/1/.

API DetailView

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".

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.