Django REST framework: CRUD API with JWT Authentication
- EN
- PT
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:
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
andPOST
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.
When accessing the url passing some id, we can see the RetrieveUpdateDestroyAPIView
in action, with options to view, change or delete a specific resource.
##
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
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/
Enter your credentials and access the 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.
- Install Django REST framework Simple JWT
pip install djangorestframework-simplejwt
- 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.
- 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:
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:
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:
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.
- Access Django Admin
- Click on “Groups”
- Create a group called
"read_only"
and give the permissionTodo_Api | todo | Can view todo
- Save
- 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:
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:
Now with the user being part of the read_only
group:
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. 😁