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:
- Create Django project with Django Rest Framework
- Add OpenApi specs to generate dynamically API documentation
- 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 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):
- 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
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)
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 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
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. 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
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):
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. This will handle automatic URL routing to Django for us.
Change urls.py
(in restapi folder):
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):
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?).
# 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.
Let's add it in urls.py
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 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
from rest_framework import permissions
And then in our specific view (let's use ProfileViewSet for this) add:
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
Top comments (0)