Create proper REST API with Django, DRF, JWT and OpenApi
Tags: django, python, openapi
Published: 2020-09-06 18:26:13

#

# *Summary of this post.*
In this post I’ll introduce following concepts, feel free to skip to the next post, if you are familiar with them already:

1. Create Django project with Django Rest Framework
2. Add OpenApi specs to generate dynamically API documentation
3. Use JWT (JSON Web Token) for authentication and authorization

## Step #1 - create new project
Start a new project (either with PyCharm -> new Django project or `django-admin startproject restapi_article`). I won't cover basics, because You can always go to [Django Tutorial](https://docs.djangoproject.com/en/3.1/intro/tutorial01/) and learn from official documentation (which I find the best at the time of writing this article).

Once we have Django project set up we need to install python libraries below (for best experience You should use [virtual environment](https://virtualenv.pypa.io/en/latest/installation.html)):
* Django
* djangorestframework
* drf-yasg
* djangorestframework-simplejwt

It's always good to conform to standards, so we should create `requirements.txt` file with all libraries. You can create it by using this command in the project root folder `pip freeze > requirements.txt`. The file should look like this:
```
asgiref==3.2.10
certifi==2020.6.20
chardet==3.0.4
coreapi==2.3.3
coreschema==0.0.4
Django==3.1.1
djangorestframework==3.11.1
djangorestframework-simplejwt==4.4.0
drf-yasg==1.17.1
idna==2.10
inflection==0.5.1
itypes==1.2.0
Jinja2==2.11.2
MarkupSafe==1.1.1
packaging==20.4
PyJWT==1.7.1
pyparsing==2.4.7
pytz==2020.1
requests==2.24.0
ruamel.yaml==0.16.12
ruamel.yaml.clib==0.2.2
six==1.15.0
sqlparse==0.3.1
uritemplate==3.0.1
urllib3==1.25.10
```

Next create an application with command `python3 manage.py startapp restapi`

Your file structure after creation should look like this:
```
. 
├── restapi
│   ├── migrations
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── restapi_article
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── venv
├── db.sqlite3
├── manage.py
└── requirements.txt
```

And add to our `restapi_article\settings.py` those three apps
```python
INSTALLED_APPS = [
    ...
    'rest_framework',
    'drf_yasg',
    'restapi',
]
```

Edit `urls.py` to use root restapi.urls as main API application urls, this way we will have `localhost:8000/admin` to play with Django admin panel, and `/api/v1` endpoint as prefix for all urls (which is a really good way to version your API, as it might easily change in the future)

```python
from django.urls import path, include

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

## Step 2 - create data models

I want to have a Drinking Diary application, where I can select a day and then post a note for myself. Also, I want to have a user profile with Bio field, location and date of Birth.

Profile and Day will be linked as Many-To-Many relationship through the DrinkingDay model where I can post a note for a given day (what I drank and with whom for example).

*TODO (future): create an Event model, where you can ask some people to join you in drinking. It will have to combine multiple Profiles/Users with one Date*

We will also use [signals](https://docs.djangoproject.com/en/3.1/topics/signals/) to create hooks for User creation, so we need only to create or update the Profile model, and User will be created/updated automatically with one request.

`models.py`
```python
from django.contrib.auth.models import User
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver


class Day(models.Model):
    day = models.DateField()


class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(max_length=500, blank=True)
    location = models.CharField(max_length=30, blank=True)
    birth_date = models.DateField(null=True, blank=True)
    days = models.ManyToManyField(Day, through='DrinkingDay')


class DrinkingDay(models.Model):
    profile = models.ForeignKey(Profile, on_delete=models.CASCADE)
    day = models.ForeignKey(Day, on_delete=models.CASCADE)
    note = models.TextField(max_length=1000, blank=True)

# Use signals so our Profile model will be automatically created
# or updated when we create or update User instance
# https://docs.djangoproject.com/en/3.1/topics/signals/
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)


@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
    instance.profile.save()
```

Now let's create and apply migrations for DB and then create superuser to be able to login to our admin panel. You need to run those three commands one by one (you can change email and username of course)

```
python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser --email admin@example.com --username admin
```

## Step 3 - create serializers
Now we need to add [serializers](https://www.django-rest-framework.org/api-guide/serializers/). Serializer in shortcut is a way to create JSON from an object. 
Let's start writing serializers for User and Profile model (we need to do them both, as it won't work without User):

`serializers.py`
```python
from django.contrib.auth.models import User
from rest_framework import serializers

from restapi.models import Profile


class UserSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = User
        fields = ['url', 'username', 'email', 'groups']


class ProfileSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = Profile
        fields = ['user', 'bio', 'location', 'birth_date', 'days']
```


## Step 4 - create viewsets
Now that we have serializers in place, we can create Views in Django. So at every url we will see what we want. Let's do that for both UserView and ProfileView (although here we could omit User view, as it's not required.

Change `views.py` (in restapi folder):
```python
from django.contrib.auth.models import User
from rest_framework import viewsets

from restapi.models import Profile
from restapi.serializers import ProfileSerializer, UserSerializer


class UserViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows users to be viewed or edited.
    """
    queryset = User.objects.all().order_by('-date_joined')
    serializer_class = UserSerializer


class ProfileViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows profiles to be viewed or edited.
    """
    queryset = Profile.objects.all()
    serializer_class = ProfileSerializer
```

## Step 5 - register router for Viewsets
DRF allows us to use something called [routers](https://www.django-rest-framework.org/api-guide/routers/). This will handle automatic URL routing to Django for us.

Change `urls.py` (in restapi folder):
```python
from django.urls import path, include
from rest_framework import routers

from restapi import views

router = routers.DefaultRouter()
router.register(r'profile', views.ProfileViewSet)
router.register(r'user', views.UserViewSet)

urlpatterns = [
    path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
    path('', include(router.urls)),
]
```

Now we can run our app with `python manage.py runserver` and go to `http://127.0.0.1:8000/api/v1/` to see that our application is working :)

If you created a superuser, that should automatically create Profile for him, so going to `http://127.0.0.1:8000/api/v1/profile/1/` should allow you to update your superuser Profile.

## Step 6 - add Swagger documentation
Everything is already good to go, we just need to create URL for showing Swagger documentation. Also, it's a good practice to create schema_view to describe our API for other developers.

Update `urls.py` (in restapi folder):
```python
from django.urls import path, include, re_path
from drf_yasg import openapi
from drf_yasg.views import get_schema_view
from rest_framework import routers, permissions

from restapi import views

router = routers.DefaultRouter()
router.register(r'profile', views.ProfileViewSet)
router.register(r'user', views.UserViewSet)

schema_view = get_schema_view(
   openapi.Info(
      title="Drinking Day API",
      default_version='v1',
      description="This API allows us to keep a diary of our daily drinking",
      terms_of_service="https://www.scvconsultants.com",
      contact=openapi.Contact(email="michal@scvconsultants.com"),
      license=openapi.License(name="MIT License"),
   ),
   public=True,
   permission_classes=(permissions.AllowAny,),
)

urlpatterns = [
    path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
    path('', include(router.urls)),
    re_path(r'^swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
    path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
]
```
Now we should be able to see Swagger UI at:
`http://127.0.0.1:8000/api/v1/swagger/` 
and .json + .yaml schema at links below:
`http://127.0.0.1:8000/api/v1/swagger.json` 
`http://127.0.0.1:8000/api/v1/swagger.yaml`

## Step 7 - Add JWT tokens to limit access to Profile model.
First, we need to update `settings.py` to add JWT auth classes. I also add pagination as this is a great practice to use default page size for collections (we don't need to send everything at once, right?).

```python
# REST FRAMEWORK config
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 25,
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    )
}
```

Then we need to create an endpoint for grabbing and refreshing Token (as tokens **should** expire due to security reasons). You can set up it yourself with the [official documentation](https://django-rest-framework-simplejwt.readthedocs.io/en/latest/settings.html).

Let's add it in `urls.py`
```python
from rest_framework_simplejwt.views import (
    TokenObtainPairView,
    TokenRefreshView,
)

urlpatterns = [
    ...
    path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
    ...
]
```
Now you can go to: `http://127.0.0.1:8000/api/v1/token/` and send your superuser credentials as a POST and you should get a beautiful working token, which you can decode and verify [here](https://www.jsonwebtoken.io/) and it should decode to user_id that corresponds with your credentials (user_id will be probably 1).

Adding now permissions to our views is really easy, first import in `views.py`

```python
from rest_framework import permissions
```

And then in our specific view (let's use ProfileViewSet for this) add:
```python
class ProfileViewSet(viewsets.ModelViewSet, mixins.UpdateModelMixin):
    ...
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]
```
Now we can easily add users, but we can't update our Profile unless we are providing proper JWT Token. The whole permissions list is available in Rest Framework [documentation](https://www.django-rest-framework.org/api-guide/permissions/#api-reference)

## Coming next: 

* Creating CI/CD with usage of [Github](https://github.com/) and [Heroku](https://www.heroku.com/).
* Dockerization
* Integration tests for API with pytest