Django API

Learn how to code secure and powerful APIs with Django using DRF and Viewsets.

Creating an API Rest with Django - Tutorial provided by AppSeed
Creating an API Rest with Django - Tutorial provided by AppSeed

Modern web frameworks have simplified the process of building web applications. A key component of these applications is web APIs.

In this article, we will use Django and Django REST Framework to develop a web API focused on authentication and user management. Our API will not only facilitate authentication but also provide capabilities for editing user information. Let's get started. 🚀

🕮 Content provided by KOLAWOLE MANGABO, a passionate developer and author of Full Stack Django and React (best seller)

Setting up the environment

Before starting, let's quickly set up the coding environment. We will start by creating the project directory and a Python virtual environment.

mkdir api-project && cd api-project
python3 -m venv venv # to create a virtual environment. 

source venv/bin/activate

Then, we can install the required tools to start coding, such as django and djangorestframework, tools that we will use to create the REST API. For authentication, we will use JWT (JSON Web token) and also a package to configure the project CORS.

pip install django djangorestframework pyjwt django-cors-headers

With the tools needed for development, we can create the project and start coding.

Creating the project

Inside the working directory, run the following commands to create a Django project called core.

django-admin startproject core .

The core at the end of the command is important as it tells Django to generate the directories and the files in the current directory.
Once it's done, create a package called api. This package will contain all the apps needed to build authentication and the CRUD operation user feature into the API.

A package in Python is a directory containing a __init__.py file. Python will treat this directory as a package containing collections of objects you can import and work with.

The api package will contain two Django applications :

  • user to handle user creation and other CRUD operations
  • authentication to deal with everything that concerns authentication, but also sessions.
    Let's start by adding the user application.

Adding User logic

In the api directory, create a Django application called user. Well, we need to build the user feature before adding authentication, as it will mostly depend on the existence of users.

django-admin startapp user

Great! Inside. the user directory, let's edit the models.py file so we can add everything needed to create a simple user, a superuser, and the fields of the User table.

# api/user/models.py

from django.db import models

from django.contrib.auth.models import (
    AbstractBaseUser,
    BaseUserManager,
    PermissionsMixin,
)


class UserManager(BaseUserManager):
    def create_user(self, username, email, password=None, **kwargs):
        """Create and return a `User` with an email, username and password."""
        if username is None:
            raise TypeError("Users must have a username.")
        if email is None:
            raise TypeError("Users must have an email.")

        user = self.model(username=username, email=self.normalize_email(email))
        user.set_password(password)
        user.save(using=self._db)

        return user

    def create_superuser(self, username, email, password):
        """
        Create and return a `User` with superuser (admin) permissions.
        """
        if password is None:
            raise TypeError("Superusers must have a password.")
        if email is None:
            raise TypeError("Superusers must have an email.")
        if username is None:
            raise TypeError("Superusers must have an username.")

        user = self.create_user(username, email, password)
        user.is_superuser = True
        user.is_staff = True
        user.save(using=self._db)

        return user


class User(AbstractBaseUser, PermissionsMixin):
    username = models.CharField(db_index=True, max_length=255, unique=True)
    email = models.EmailField(db_index=True, unique=True, null=True, blank=True)
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)
    date = models.DateTimeField(auto_now_add=True)

    USERNAME_FIELD = "email"
    REQUIRED_FIELDS = ["username"]

    objects = UserManager()

    def __str__(self):
        return f"{self.username}"

In the code above, we are adding two objects: the User model class and the UserManager manager class. In Django, the models describe the shape of the data to the Django ORM, so it can handle SQL queries such as the creation of the table, the addition of a field, an SQL SELECT query, and much much more.
On the User model, we have fields such as username, email, is_active, is_staff, date that constitute enough details for a user.
A manager in Django acts as a bridge between the database and the Django model. It contains all collections of methods and classes needed to interact with the database.
In the UserManager class, we are writing the create_user and create_superuser methods for our logic of creating users. It is interesting to create a custom User and UserManager when you are writing your authentication logic or just modifying the way the User table should behave in the project.

Adding User serializer

To make the communication effective in the API, mostly using JSON for payload requests or responses, we need to add a serializer.
In the user application, create a file called serializers.py. This file will contain the code for the UserSerializer.

# api/user/serializers.py

from api.user.models import User
from rest_framework import serializers


class UserSerializer(serializers.ModelSerializer):
    date = serializers.DateTimeField(read_only=True)

    class Meta:
        model = User
        fields = ["id", "username", "email", "date"]
        read_only_field = ["id"]from api.user.models import User

With the UserSerializer added, we can then move to add the viewset, which will act as a controller for the /api/user/ endpoint. Let's add the UserViewset.

Adding User viewset

In the user directory, add a file called viewsets.py. This file will contain the code for the UserViewSet class. A viewset helps us quickly write the logic for a controller in just a few lines.

Creating Users

# api/user/viewsets.py

from api.user.serializers import UserSerializer
from api.user.models import User
from rest_framework import viewsets, status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.exceptions import ValidationError
from rest_framework import mixins


class UserViewSet(
    viewsets.GenericViewSet, mixins.CreateModelMixin, mixins.UpdateModelMixin
):
    serializer_class = UserSerializer
    permission_classes = (IsAuthenticated,)

    error_message = {"success": False, "msg": "Error updating user"}

    def update(self, request, *args, **kwargs):
        partial = kwargs.pop("partial", True)
        instance = User.objects.get(id=request.data.get("userID"))
        serializer = self.get_serializer(instance, data=request.data, partial=partial)
        serializer.is_valid(raise_exception=True)
        self.perform_update(serializer)

        if getattr(instance, "_prefetched_objects_cache", None):
            instance._prefetched_objects_cache = {}

        return Response(serializer.data)

    def create(self, request, *args, **kwargs):
        user_id = request.data.get("userID")

        if not user_id:
            raise ValidationError(self.error_message)

        if self.request.user.pk != int(user_id) and not self.request.user.is_superuser:
            raise ValidationError(self.error_message)

        self.update(request)

        return Response({"success": True}, status.HTTP_200_OK)

In the code above, we are creating a viewsets and only allowing POST requests to this controller to edit user information.

{  
	"username": "test",  
	"password": "pass",  
	"email": "test@appseed.us",
	"userID": 4
}

Now that we have the user viewsets, we can code the authentication feature of the API.

Authentication

In the api directory, run the Django command to create a Django application called authentication. This application will contain all logic for authentication but also logic to handle sessions.

django-admin startapp authentication

Let's get to work. Replace the models.py file with Python package models. Inside this package, add a file called active_session. This file will contain the code of the ActiveSession model.

# api/authentication/models/active_session.py

from django.db import models


class ActiveSession(models.Model):
    user = models.ForeignKey("api_user.User", on_delete=models.CASCADE)
    token = models.CharField(max_length=255)
    date = models.DateTimeField(auto_now_add=True)

Now that we have the models to handle sessions, let's create a backend class to handle authentication.

Adding an auth backend

Inside the authentication directory, create a file called backends.py.

# api/authentication/backends.py

import jwt

from rest_framework import authentication, exceptions
from django.conf import settings

from api.user.models import User
from api.authentication.models import ActiveSession


class ActiveSessionAuthentication(authentication.BaseAuthentication):

    auth_error_message = {"success": False, "msg": "User is not logged on."}

    def authenticate(self, request):

        request.user = None

        auth_header = authentication.get_authorization_header(request)

        if not auth_header:
            return None

        token = auth_header.decode("utf-8")

        return self._authenticate_credentials(token)

    def _authenticate_credentials(self, token):

        try:
            jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
        except:
            raise exceptions.AuthenticationFailed(self.auth_error_message)

        try:
            active_session = ActiveSession.objects.get(token=token)
        except:
            raise exceptions.AuthenticationFailed(self.auth_error_message)

        try:
            user = active_session.user
        except User.DoesNotExist:
            msg = {"success": False, "msg": "No user matching this token was found."}
            raise exceptions.AuthenticationFailed(msg)

        if not user.is_active:
            msg = {"success": False, "msg": "This user has been deactivated."}
            raise exceptions.AuthenticationFailed(msg)

        return (user, token)

The ActiveSessionAuthentication class in Django REST API operates as follows, with a focus on JWT (JSON Web Token) handling:

  1. Initialization: The process begins by setting request.user to None and retrieving the authorization header from the incoming request.
  2. Header Verification: If no authorization header is present, the method returns None, implying that no authentication is attempted.
  3. Token Extraction: The method decodes the JWT token from the authorization header. JWT tokens are compact, URL-safe means of representing claims to be transferred between two parties.
  4. JWT Validation:
    • The token is decoded using Django's SECRET_KEY. This step verifies the token's signature and ensures it was not tampered with.
    • The decoding also checks the token's expiration (exp claim). If the token is expired or invalid, an authentication failed exception with an appropriate error message is raised.
  5. Active Session Check:
    • It looks for an ActiveSession object that matches the token. The presence of an active session indicates that the user previously logged in and the session is still valid.
    • If no matching active session is found, it suggests either the token is invalid or the session has ended, leading to an authentication failed exception.
  6. User Verification:
    • The user associated with the active session is retrieved.
    • It checks if the user exists and is active. If the user does not exist or has been deactivated, an authentication failed exception with a specific message is raised.
  7. Successful Authentication:
    • If all validations are passed, the method returns the user and the token.
    • This successful return allows the user to proceed with actions that require authentication.

With the authentication class added, let's configure REST_FRAMEWORK so it can use this backend for authentication.
In the core/settings.py file, add these few lines of code at the end.

# core/settings.py

...

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": (
        "api.authentication.backends.ActiveSessionAuthentication",
    ),
    "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
}

With the authentication backend added, we can quickly move to the register, login, and logout feature.

Adding Registration and Login serializers

In the authentication package, add a file called serializers.py. This file will contain code for the LoginSerializer and the RegisterSerializer.


# api/authentication/serializers.py

import jwt
from rest_framework import serializers, exceptions
from django.contrib.auth import authenticate
from datetime import datetime, timedelta
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist

from api.authentication.models import ActiveSession



def _generate_jwt_token(user):
    token = jwt.encode(
        {"id": user.pk, "exp": datetime.utcnow() + timedelta(days=7)},
        settings.SECRET_KEY,
    )

    return token


class LoginSerializer(serializers.Serializer):
    email = serializers.CharField(max_length=255)
    username = serializers.CharField(max_length=255, read_only=True)
    password = serializers.CharField(max_length=128, write_only=True)

    def validate(self, data):
        email = data.get("email", None)
        password = data.get("password", None)

        if email is None:
            raise exceptions.ValidationError(
                {"success": False, "msg": "Email is required to login"}
            )
        if password is None:
            raise exceptions.ValidationError(
                {"success": False, "msg": "Password is required to log in."}
            )
        user = authenticate(username=email, password=password)

        if user is None:
            raise exceptions.AuthenticationFailed({"success": False, "msg": "Wrong credentials"})

        if not user.is_active:
            raise exceptions.ValidationError(
                {"success": False, "msg": "User is not active"}
            )

        try:
            session = ActiveSession.objects.get(user=user)
            if not session.token:
                raise ValueError

            jwt.decode(session.token, settings.SECRET_KEY, algorithms=["HS256"])

        except (ObjectDoesNotExist, ValueError, jwt.ExpiredSignatureError):
            session = ActiveSession.objects.create(
                user=user, token=_generate_jwt_token(user)
            )

        return {
            "success": True,
            "token": session.token,
            "user": {"_id": user.pk, "username": user.username, "email": user.email},
        }

class RegisterSerializer(serializers.ModelSerializer):
    password = serializers.CharField(min_length=4, max_length=128, write_only=True)
    username = serializers.CharField(max_length=255, required=True)
    email = serializers.EmailField(required=True)

    class Meta:
        model = User
        fields = ["id", "username", "password", "email", "is_active", "date"]

    def validate_username(self, value):
        try:
            User.objects.get(username=value)
        except ObjectDoesNotExist:
            return value
        raise ValidationError({"success": False, "msg": "Username already taken."})

    def validate_email(self, value):
        try:
            User.objects.get(email=value)
        except ObjectDoesNotExist:
            return value
        raise ValidationError({"success": False, "msg": "Email already taken."})

    def create(self, validated_data):

        return User.objects.create_user(**validated_data)


The LoginSerializer validates user login by requiring email and password. It authenticates the user, checks active status, manages JWT token sessions, and addresses errors like incorrect credentials or expired tokens.


The RegisterSerializer handles user registration. It confirms the uniqueness of usernames and emails and creates a new user with the provided valid data, including password, username, and email.

With the serializers for the login and the registration feature added, we can then code the viewsets for this package.

Adding Registration and Login viewsets

In the authentication directory, create a file called viewsets.py. This file will contain the code for the register, login, and logout viewsets. We will also add a viewset that will help us check if there is an active session.


# api/authentication/viewsets.py

from rest_framework import viewsets, mixins  
from rest_framework.response import Response  
from rest_framework import status  
from rest_framework.permissions import AllowAny  
  
from api.authentication.serializers import LoginSerializer, RegisterSerializer  
from api.authentication.models import ActiveSession  
  
class LoginViewSet(viewsets.GenericViewSet, mixins.CreateModelMixin):  
    permission_classes = (AllowAny,)  
    serializer_class = LoginSerializer  
  
    def create(self, request, *args, **kwargs):  
        serializer = self.get_serializer(data=request.data)  
  
        serializer.is_valid(raise_exception=True)  
  
        return Response(serializer.validated_data, status=status.HTTP_200_OK)

class RegisterViewSet(viewsets.ModelViewSet):  
    http_method_names = ["post"]  
    permission_classes = (AllowAny,)  
    serializer_class = RegisterSerializer  
  
    def create(self, request, *args, **kwargs):  
        serializer = self.get_serializer(data=request.data)  
  
        serializer.is_valid(raise_exception=True)  
        user = serializer.save()  
  
        return Response(  
            {  
                "success": True,  
                "userID": user.id,  
                "msg": "The user was successfully registered",  
            },  
            status=status.HTTP_201_CREATED,  
        )

class LogoutViewSet(viewsets.GenericViewSet, mixins.CreateModelMixin):  
    permission_classes = (IsAuthenticated,)  
  
    def create(self, request, *args, **kwargs):  
        user = request.user  
  
        session = ActiveSession.objects.get(user=user)  
        session.delete()  
  
        return Response(  
            {"success": True, "msg": "Token revoked"}, status=status.HTTP_200_OK  
        )

class ActiveSessionViewSet(viewsets.GenericViewSet, mixins.CreateModelMixin):  
    http_method_names = ["post"]  
    permission_classes = (IsAuthenticated,)  
  
    def create(self, request, *args, **kwargs):  
        return Response({"success": True}, status.HTTP_200_OK)

The LoginViewSet in Django REST API handles user login, using the LoginSerializer to validate data, create a session upon successful authentication, and return a response with appropriate HTTP status.

The RegisterViewSet deals with user registration, using the RegisterSerializer to validate and create new user data, and responds with user ID and success message upon successful registration.

The LogoutViewSet manages user logout in Django REST API. It invalidates the user's session by deleting it and responds with a confirmation message.

The ActiveSessionViewSet provides a method to check active sessions for authenticated users, simply returning a success response.

With the viewsets added, we can now register them in the API routers and start making requests.

Registering the viewsets

In the api directory, create a file called routers.py. This file will contain declarations of the endpoints of the API. This is where we will register the viewsets we have created.

# api/routers.py

from api.authentication.viewsets import (  
    RegisterViewSet,  
    LoginViewSet,  
    ActiveSessionViewSet,  
    LogoutViewSet,  
)  
from rest_framework import routers  
from api.user.viewsets import UserViewSet  
  
router = routers.SimpleRouter(trailing_slash=False)  
  
router.register(r"edit", UserViewSet, basename="user-edit")  
  
router.register(r"register", RegisterViewSet, basename="register")  
  
router.register(r"login", LoginViewSet, basename="login")  
  
router.register(r"checkSession", ActiveSessionViewSet, basename="check-session")  
  
router.register(r"logout", LogoutViewSet, basename="logout")  
  
  
urlpatterns = [  
    *router.urls,  
]

And finally, to make it accessible from the Django server, let's register urlspatterns in core/urls.py.

# core/urls.py

from django.urls import path, include  
from django.contrib import admin  
from api.authentication.viewsets.social_login import GithubSocialLogin  
  
urlpatterns = [  
    path('admin/', admin.site.urls),  
    path("api/users/", include(("api.routers", "api"), namespace="api"))
]

We have implemented the REST API for authentication and user edition. Let's start the server and start testing.

python manage.py runserver makemigrations

python manage.py runserver migrate

python manage.py runserver

The server will be accessible at http://localhost:8000. Here are the endpoints and some data you can use for the requests.

Register - api/users/signup
POST api/users/signup  
Content-Type: application/json  
  
{  
    "username": "test",    
    "password": "pass",    
    "email": "test@appseed.us"  
}  
Login - api/users/login
POST /api/users/login  
Content-Type: application/json  
  
{  
    "password": "pass",    
    "email": "test@appseed.us"  
}  
Logout - api/users/logout
POST api/users/logout  
Content-Type: application/json  
authorization: JWT_TOKEN (returned by Login request)  
  
{  
    "token": "JWT_TOKEN"
}  

You now have a fully operational Django REST API, complete with authentication and permissions. This forms an excellent base for a range of projects, providing a reliable and secure framework to build upon. It's a solid starting point for any web application.

Happy coding! 🚀

In this article, we have learned how to build a REST API with Django and Django REST exploring concepts such as authentication with JWT, permissions, and logout.

You can find the code for this article here.

For more resources, feel free to acces: