Skip to main content

João Senger

Django REST framework: CRUD API with JWT Authentication

Table of Contents

Learn how to create a professional REST API using Django REST framework. Build a complete CRUD with user authentication using JWT.

# Introduction

## What will we learn?

Our project will be to create an API for managing tasks (To-Do). We’ll use Django users and permissions to access API resources, plus JWT authentication.

The project code can be found at this link.

This article is not for complete beginners, topics like what is a JWT token, what is a REST API or what is the HTTP protocol will not be covered.

## What is Django REST framework?

Django REST framework is a toolkit for building Web APIs within the Django ecosystem. It has several ready-made implementations, such as Serializers, Views, authentication and permissions.

## What are the benefits of using a Framework for development?

  • Security: protection against common vulnerabilities, such as SQL Injection, Cross-Site Scripting (XSS) and Cross-Site Request Forgery (CSRF)

  • ORM (Object-Relational Mapping): facilitating work with relational databases, allowing you to manipulate the database using ready-made objects instead of SQL queries

  • Community: collaborative development, several programmers test the tool and contribute to the framework’s evolution

  • Development speed: ready-made functionalities can be used for quick and secure project implementation

  • Maintenance: clean and organized code ensures ease in maintenance and implementation of new features

  • Reliability: popular frameworks (like DRF - Django REST framework) are widely used and tested, reducing the probability of bugs and failures.

# Prerequisites

## Knowledge:

  • Basic knowledge about REST API
  • Basic knowledge in web development
  • Python: basic knowledge and object orientation
  • Basic knowledge about HTTP protocol

## Software:

  • Python 3.8 or higher
  • Code editor (I recommend VSCode)
  • Postman or Insomnia (optional)

# Initial setup

First, let’s create a virtual environment for our project:

python -m venv venv
source venv/bin/activate # for Linux/MacOS users
.\venv\Scripts\Activate.ps1 # for Windows users

With the environment created, let’s install Django and Django Rest Framework (DRF):

pip install django && pip install djangorestframework

## Starting and configuring the project

Now that we have our environment with Django and DRF installed, let’s start a new Django project:

django-admin startproject app .

We can test our new project by running the command:

python manage.py runserver

When accessing http://127.0.0.1:8000/ a Django ‘welcome’ page should be displayed:

Django-welcome

We can stop the server by pressing CTRL+C.

## Creating our first application

With our Django project created, we can create a new application, which will be called todo_api:

python manage.py startapp todo_api

Important: our project is called app and in this project we have an application called todo_api, don’t confuse them!

A new directory called todo_api was created in our project. We need to add Django REST framework and our application to our project app settings, for this, access the file app/settings.py and add todo_api to INSTALLED_APPS:

# app/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',

    'todo_api',
]

Now we have our project and application configured, we can make our first database operation, which creates an SQLite database and Django’s initial tables:

python manage.py migrate

# API Development

With the project and application created and configured, we can start developing the API itself.

We’ll start developing our Model, that is, the model of our API in the database, which will be simple.

## Models and Serializers

Our model will have only three fields: id, task and done, being:

  • id: automatically created by Django, we don’t need to declare it (how convenient!)
  • task: will be the name of our “task”, will be a CharField type data
  • done: will be the status of our task, will be a boolean (true/false)

Open the file todo_api/models.py and insert the following code:

# todo_api/models.py
from django.db import models


class Todo(models.Model):
    task = models.CharField(max_length=250)
    done = models.BooleanField()

    def __str__(self):
        return self.task

Django will interpret this model and automatically create the table in the database according to our specifications, we don’t need to worry about SQL, this is one of the facilities that Django ORM brings us.

For Django to “read” the models files of our project, we need to run the command:

python manage.py makemigrations

After that, we run the migrate command to apply:

python manage.py migrate

A table will be created in your local database (SQLite) with the name todo_todo_api, that is: modelname_appname. The id, task and done fields will also be created.

Our model was created, however, API data must travel in JSON format, not in database object format, to “serialize” our data from object to json, we need to create a serializer.

As everything in Django is simple, this functionality is already ready, just use it!

Create a file called serializers.py inside your todo_api app: todo_api/serializers.py and insert the code below:

# todo_api/serializers.py
from rest_framework import serializers
from todo_api.models import Todo


class TodoSerializer(serializers.ModelSerializer):

    class Meta:
        model = Todo
        fields = '__all__'

Basically we import our Todo Model, which is the representation of our data in the database and use Django REST framework’s ModelSerializer, which will serialize automatically. We also inform that we want to serialize all fields with fields = __all__

## Views and View Classes

The todo_api/views.py file is responsible for our API’s business logic, here all the logic of our CRUD will be implemented. As our API is simple, we’ll use two ready-made DRF classes to abstract operations, being:

  • ListCreateAPIView: responsible for GET and POST verbs, lists all tasks and allows creating a new task.

  • RetrieveUpdateDestroyAPIView: works with task ids, here we can use all http verbs: GET, POST, PUT, DELETE. We can list, change or delete tasks according to their unique id.

Inside the file todo_api/views.py insert the code:

# todo_api/views.py
from rest_framework import generics
from todo_api.models import Todo
from todo_api.serializers import TodoSerializer


class TodoCreateListView(generics.ListCreateAPIView):
    queryset = Todo.objects.all()
    serializer_class = TodoSerializer


class TodoRetrieveUpdateDestroyView(generics.RetrieveUpdateDestroyAPIView):
    queryset = Todo.objects.all()
    serializer_class = TodoSerializer

We created two classes, one for each DRF View Class, now, we can configure our URLs to send requests to our views.

# URLs and API Versioning

Inside the todo_api directory create a file called urls.py and insert the code below:

# todo_api/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('todos/', views.TodoCreateListView.as_view(), name='todo-create-list-view'),
    path('todos/<int:pk>', views.TodoRetrieveUpdateDestroyView.as_view(), name='todo-detail-view'),
]

This piece of code informs that when accessing our api on the route /todos/ and /todos/id/ Django should use the view configured previously.

It’s not a good practice to access routes directly, the good practice is to use url versioning, that is, before accessing the path /todos/, for example, we access /api/v1/todos/

This ensures that in a new implementation, in case of a v2, clients using the /v1/ endpoint are not affected.

To implement versioning, access the file app/urls.py and insert the code:

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

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/v1/', include('todo_api.urls')),
]

We use Django’s include to create versioning, now, our api will be accessed by: http://127.0.0.1:8000/api/v1/todos/

## API working, first test!

Our API is already functional, we can test it.

python manage.py runserver

Access http://127.0.0.1:8000/api/v1/todos/ and create some tasks.

drf-post

When accessing the url passing some id, we can see the RetrieveUpdateDestroyAPIView in action, with options to view, change or delete a specific resource.

drf-get-put-delete

## Django ADMIN - User creation and management

Django already brings us a ready platform to manage users, user groups and database Models.

To create the first user (root) type in the terminal:

python manage.py createsuperuser

django-create-superuser

To add our model to Django Admin, access the file todo_api/admin and insert the code:

# todo_api/admin.py
from django.contrib import admin
from todo_api.models import Todo


@admin.register(Todo)
class TodoAdmin(admin.ModelAdmin):
    list_display = ('id', 'task', 'done')

Run the server:

python manage.py runserver

Access the django admin url: http://127.0.0.1:8000/admin/

django-admin

Enter your credentials and access the dashboard.

django-dashboard

Create at least one user, without admin credentials for us to proceed.

## Security - JWT Authentication and endpoint protection

With our CRUD ready and django admin managing users, we can start our API authentication.

We’ll require that only authenticated users have access to the API, plus we’ll protect our endpoints, where only authenticated and permitted users can access the API and execute actions.

  1. Install Django REST framework Simple JWT
pip install djangorestframework-simplejwt
  1. In the project configuration, app/settings.py add the code below at the end of the file:
# app/settings.py
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    )
}

SIMPLE_JWT = {
    "ACCESS_TOKEN_LIFETIME": timedelta(days=1),
    "REFRESH_TOKEN_LIFETIME": timedelta(days=7),
}

At the beginning of the file, import timedelta:

from datetime import timedelta

In this code we configure that authentication will be done by Simple JWT and what will be the token lifetime before it expires.

  1. Creating the endpoint to generate the token

We need to create an endpoint to generate our jwt token. We’ll create a new application to be responsible for authentication:

python manage.py startapp authentication

Add the new app in the file app/settings.py:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'rest_framework',
    'todo',
    'authentication',
]

Inside this new app, create a file called urls.py and insert the code:

# authentication/urls.py
from django.urls import path
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView, TokenVerifyView

urlpatterns = [
    path('authentication/token/', TokenObtainPairView.as_view(), name='token-obtain-pair-view'),
    path('authentication/token/refresh/', TokenRefreshView.as_view(), name='token-refresh-vier'),
    path('authentication/token/verify', TokenVerifyView.as_view(), name='token-verify-view')
]

We’re creating endpoints to generate our JWT token, its refresh and verify.

All this is abstracted by DRF, we don’t need to create everything by hand.

Now, in the file app/urls.py we’ll import our new endpoints, the code should look like this:

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

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/v1/', include('authentication.urls')), # endpoint for authentication
    path('api/v1/', include('todo_api.urls')),
]

Now, let’s protect our views. In the file todo/views.py we’ll add permission_classes to our routes, this is a DRF resource used to protect routes. The file should look like this:

# todo/views.py
from rest_framework import generics
from rest_framework.permissions import IsAuthenticated
from todo_api.models import Todo
from todo_api.serializers import TodoSerializer


class TodoCreateListView(generics.ListCreateAPIView):
    permission_classes = (IsAuthenticated, )
    queryset = Todo.objects.all()
    serializer_class = TodoSerializer


class TodoRetrieveUpdateDestroyView(generics.RetrieveUpdateDestroyAPIView):
    permission_classes = (IsAuthenticated, )
    queryset = Todo.objects.all()
    serializer_class = TodoSerializer

Now, when trying to access any resource from our API, it won’t be possible. See:

drf-IsAuthenticated

To access, it will be necessary to generate a JWT Token on the route api/v1/authentication/token/.

From here, we’ll use Postman to make requests to the API, as we’ll have to inform the token in our requests.

Access Postman and make a POST request to the url 127.0.0.1:8000/api/v1/authentication/token/ passing the username/password in the request body:

{
  "username": "yourusername",
  "password": "yourpassword"
}

When making the request informing the user and password you created in Django Admin, an Access Token and a Verify Token will be returned, however, in this tutorial we’ll only use the access token:

drf-simplejwt

Copy the access token and make a new request on the route 127.0.0.1:8000/api/v1/todos passing the copied token in Authorization > Bearer Token, as shown in the image below:

drf-jwt-authenticated

With this, only authenticated users can access our tasks. But what if I want some user to have read-only permission, but not data modification? that is, can do a GET, but not a PUT? To solve this problem, we’ll use user permissions.

## User Permissions

At this point only authenticated users can use the API, however, we want to restrict access for these users. For example, a certain user may only have read permission GET, while another user can read and create new tasks GET and POST, while another type of user can do all actions. For this, we’ll use Django user permissions.

Let’s create two examples, the first will be a group of users that can only do GET and the second with all permissions.

  1. Access Django Admin
  2. Click on “Groups”
  3. Create a group called "read_only" and give the permission Todo_Api | todo | Can view todo
  4. Save
  5. Repeat the process, now create a group called "full_access" and give the permissions: Todo_Api | todo | Can view todo
    Todo_Api | todo | Can add todo
    Todo_Api | todo | Can delete todo
    Todo_Api | todo | Can change todo

Example of creating the “full_access” group:

django-admin-group

Now we have two user groups with different permissions, this is the best practice for user permission management.

Now, we need to inform in the code that users besides being authenticated must have permission to execute actions in the API.

Inside app create a file called permissions.py and insert the code below:

# app/permissions.py
from rest_framework import permissions


class GlobalDefaultPermission(permissions.BasePermission):

    def has_permission(self, request, view):
        model_permission_codename = self.__get_model_permission_codename(
            method=request.method,
            view=view,
        )

        if not model_permission_codename:
            return False

        return request.user.has_perm(model_permission_codename)

    def __get_model_permission_codename(self, method, view):
        try:
            model_name = view.queryset.model._meta.model_name
            app_label = view.queryset.model._meta.app_label
            action = self.__get_action_sufix(method)
            return f'{app_label}.{action}_{model_name}'
        except AttributeError:
            return None

    def __get_action_sufix(self, method):
        method_actions = {
            'GET': 'view',
            'POST': 'add',
            'PUT': 'change',
            'PATCH': 'change',
            'DELETE': 'delete',
            'OPTIONS': 'view',
            'HEAD': 'view',
        }
        return method_actions.get(method, '')

This code overwrites the has_permission function inherited from permissions.BasePermission. This function should return true or false. The code checks if the user has attached permission and returns true or false.

Finally, let’s add this permission class to our views file. In the file todo_api/views.py import the GlobalDefaultPermission class created previously and include it in the permission_classes. The code should look like this:

# todo_api/views.py
from rest_framework import generics
from rest_framework.permissions import IsAuthenticated
from todo_api.models import Todo
from todo_api.serializers import TodoSerializer
from app.permissions import GlobalDefaultPermission


class TodoCreateListView(generics.ListCreateAPIView):
    permission_classes = (IsAuthenticated, GlobalDefaultPermission)
    queryset = Todo.objects.all()
    serializer_class = TodoSerializer


class TodoRetrieveUpdateDestroyView(generics.RetrieveUpdateDestroyAPIView):
    permission_classes = (IsAuthenticated, GlobalDefaultPermission)
    queryset = Todo.objects.all()
    serializer_class = TodoSerializer

# Tests

In Django Admin, create a user and put them in the read_only group, it will work normally, then try to register a new task, you won’t have permission. After that, add the full_access group to the user and try again, you’ll be able to create the task.

Example of access attempt with a user without permission:

sem-permissao

Now with the user being part of the read_only group:

sem-permissao

Thus, our API is complete. We perform CRUD operations with authenticated users and protected routes.

# Conclusion

In this article we learned to use Django REST framework to create a REST API in a simple, secure and professional way. The work of a Backend developer is often tied to CRUD operations in the database, plus data modeling and application security. This API doesn’t aim to exhaust all development best practices, but to guide beginning developers about practices used in the market.

# Next steps

We can always improve our project, in the future we’ll implement some functionalities, such as:

  • Flake8 linter configuration for code standardization according to PEP 8
  • Create API documentation with Swagger
  • Create a Dockerfile to run the application as a container
  • Deploy the API on AWS with a complete Pipeline (CodeCommit + CodePipeline + CodeBuild + ECR + ECS with Fargate)

These contents will be published soon here on the blog.

I’ll be waiting for you. 😁