Day 3 : Django Rest Framework API

Create a chat application with Ionic and Django – Series – Part three

Day 3 : Django Rest Framework API

In the previous tutorial, we saw how to secure our Chat with JWT Token

In this tutorial, we will implement our API to manage our Chat and Message models.

First we will create a python directory in the root folder of our chattuto project:

mkdir api

Then we will add a new router api route in the chattuto/urls.py file:

from django.conf.urls import include
from django.urls import path
from django.contrib import admin

urlpatterns = [
    path('chat/', include('chat.urls')),
    path('admin/', admin.site.urls),
    path('auth/', include('djoser.urls')),
    path('auth/', include('djoser.urls.jwt')),
    path('api/', include('api.urls')),
]

and add some configuration about Django Rest Framework in our chattuto/settings.py file:

REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
   'rest_framework_simplejwt.authentication.JWTAuthentication',
),
'DEFAULT_FILTER_BACKENDS': (
'django_filters.rest_framework.DjangoFilterBackend',
    'rest_framework.filters.OrderingFilter',
),
'DEFAULT_RENDERER_CLASSES': (
    'rest_framework.renderers.JSONRenderer',
    'rest_framework.renderers.BrowsableAPIRenderer',
),
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 20,
}

To secure our API we ask the framework to use the JWTAuthentication class.

We will also add new libraries to our requirements.txt, one to extend filter possibilities with Rest framework and the other one to avoid CORS issues when we will call our API from Ionic application.
The last one drf-yasg will generate the documentation for our API

Django==3.1.7
pillow==8.0.1
psycopg2==2.8.5 --no-binary psycopg2
channels==3.0.3
channels-redis==3.2.0
django-channels-jwt-auth-middleware==1.0.0
djoser==2.1.0
djangorestframework_simplejwt==4.6.0
djangorestframework==3.12.2
django-filter==2.4.0
django-cors-headers==3.7.0
drf-yasg==1.20.0

We modify once again our chattuto/settings.py file to fullfill requirements of the Corsheaders library

INSTALLED_APPS = [
    'drf_yasg',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'channels',
    'chat',
    'rest_framework',
    'djoser',
    'corsheaders',
]
MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

CORS_ALLOW_ALL_ORIGINS=True

Let’s modify our chattuto/urls.py file to include a self generated documentation for our API

from django.conf.urls import include
from django.urls import path
from django.contrib import admin
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
from rest_framework import permissions

schema_view = get_schema_view(
    openapi.Info(
        title="API",
        default_version='v1',
        description="API description",
        terms_of_service="https://www.google.com/policies/terms/",
        contact=openapi.Contact(email="contact@snippets.local"),
        license=openapi.License(name="BSD License"),
    ),
    public=True,
    permission_classes=(permissions.AllowAny,),
)
urlpatterns = [
    path('chat/', include('chat.urls')),
    path('admin/', admin.site.urls),
    path('auth/', include('djoser.urls')),
    path('auth/', include('djoser.urls.jwt')),
    path('api/', include('api.urls')),
    path('documentation/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
]

Ok now let’s implement our API. Create an api/urls.py file into our api folder:

from django.conf.urls import include, url
from .views import *
from rest_framework.decorators import api_view
from rest_framework.routers import DefaultRouter
router = DefaultRouter()

urlpatterns = [
  url(r'^user/(?P<pk>[0-9A-Fa-f-]+)/$', UserDetailView.as_view()),
  url(r'^user/$', UserListView.as_view()),

  url(r'^chat/(?P<pk>[0-9A-Fa-f-]+)/$', ChatDetailView.as_view()),
  url(r'^chat/$', ChatListView.as_view()),

  url(r'^message/(?P<pk>[0-9A-Fa-f-]+)/$', MessageDetailView.as_view()),
  url(r'^message/$', MessageListView.as_view()),

]

We added some endpoints on User, Chat and Message. now let’s write the api/serializers.py file :

from rest_framework.serializers import ModelSerializer
from chat.models import *

class UserSerializer(ModelSerializer):
    class Meta:
        ref_name="MyCustomUser"
        model = User
        fields = '__all__'
class ChatSerializer(ModelSerializer):
    class Meta:
        model = Chat
        fields = '__all__'

class MessageSerializer(ModelSerializer):
    class Meta:
        model = Message
        fields = '__all__'

The ref_name field on User Meta class is to avoid conflict with the DRF YASG library (which implement user endpoint too).

and finally an api/views.py file:

from django.contrib.auth.models import User
from rest_framework import generics, permissions
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters
from api.serializers import *
from chat.models import *
import logging

# Get an instance of a logger
logger = logging.getLogger('django')

class UserListView(generics.ListCreateAPIView):
    permission_classes = [permissions.IsAuthenticated]
    queryset = User.objects.all()
    serializer_class = UserSerializer
    filterset_fields = ['password','id','email','first_name','last_name','valid']
    filter_backends = [DjangoFilterBackend,filters.SearchFilter,filters.OrderingFilter]
    # Filter for connected user
    def get_queryset(self):
        user = self.request.user
        queryset = User.objects.filter(pk=user.id)
        return queryset

class UserDetailView(generics.RetrieveUpdateDestroyAPIView):
    permission_classes = [permissions.IsAuthenticated]
    queryset = User.objects.all()
    serializer_class = UserSerializer

class ChatListView(generics.ListCreateAPIView):
    permission_classes = [permissions.IsAuthenticated]
    queryset = Chat.objects.all()
    serializer_class = ChatSerializer
    filterset_fields = ['id']
    filter_backends = [DjangoFilterBackend,filters.SearchFilter,filters.OrderingFilter]

class ChatDetailView(generics.RetrieveUpdateDestroyAPIView):
    permission_classes = [permissions.IsAuthenticated]
    queryset = Chat.objects.all()
    serializer_class = ChatSerializer

class MessageListView(generics.ListCreateAPIView):
    permission_classes = [permissions.IsAuthenticated]
    queryset = Message.objects.all()
    serializer_class = MessageSerializer
    filterset_fields = ['id','type','isRead']
    filter_backends = [DjangoFilterBackend,filters.SearchFilter,filters.OrderingFilter]

class MessageDetailView(generics.RetrieveUpdateDestroyAPIView):
    permission_classes = [permissions.IsAuthenticated]
    queryset = Message.objects.all()
    serializer_class = MessageSerializer

And voila if you launch the server and go to url http://127.0.0.1:8000/documentation/

You should see the documentation:

API documentation

Now you can refer to this documentation to know how to do CRUDS operations for our models User, Chat,Message.

And because we secured the API with JWT, you should not be able to try the endpoints without being authenticated

Unauthenticated

which is why you can see a Django login button on the interface.

We can also identified ourselves as we learn in previous tutorial:

curl -X POST \
  http://127.0.0.1:8000/auth/jwt/create/ \
  -H 'Content-Type: application/json' \
  -d '{"email": "csurbier@idevotion.fr", "password": "YOURPASSWORD"}'

Don’t forger to replace with your user and password values

And once you have your access token, try to access the Chat api by specifying the JWT token in the Authorization header of your request:

  curl --location --request GET 'http://127.0.0.1:8000/api/chat/' \
--header 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjE2MDYxNTU0LCJqdGkiOiJiZGUwYjZkMTg1MGI0ZDlmYjJmNmViNzgwYzRlMWRjNyIsInVzZXJfaWQiOiI5MDUyYTA4MC0yNGQ4LTRhMGUtOGQ5YS03NDIyMTNjMGJmOTEifQ.H0rU-trZdO_rPSD9HAqU-mlKb8JIY52STo68iDiL8wc'

You should receive a JSON with the list of existing Chat (if you created one before with the admin):

{
    "count": 1,
    "next": null,
    "previous": null,
    "results": [
        {
            "id": "53713a91-e936-4169-9fef-4bc870a0babc",
            "createdAt": "2021-03-08T08:11:30.677821Z",
            "updatedAt": "2021-03-08T08:11:30.677921Z",
            "fromUser": "9052a080-24d8-4a0e-8d9a-742213c0bf91",
            "toUser": "b4db6ca7-21e0-4c83-b56b-f5c86c4cbc43"
        }
    ]
}

Ok now that we have our API ready to use, it’s time to dive into our Ionic application in our next tutorial.

Questions / Answers

  1. What is the name of the library used to create an API ?

    Django Rest Framework

  2. Is it possible to use the library without beeing authenticated ?

    Yes.

  3. What should we add to our settings to require a JWT authentication ?

    Specify the authentication class in the Django Rest Framework configuration dictionnary:

    REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
    'rest_framework_simplejwt.authentication.JWTAuthentication',
    ),
  4. What should we add to each http query header to be authenticated ?

Add an ‘Authorization: Bearer ‘ parameter.

Christophe Surbier