Django API Server
Open-source Django API server that explains how to extend an existing codebase
This sample explains how to extend an existing Django API Starter and add another model managed via a new API node.
Django API Server - Cover Image.

Codebase structure

1
PROJECT ROOT
2
β”œβ”€β”€ api # App containing all project-specific apps
3
β”‚Β Β  β”œβ”€β”€ apps.py
4
β”‚Β Β  β”œβ”€β”€ authentication # Implements authentication app logic (register, login, session) logic
5
β”‚Β Β  β”‚Β Β  β”œβ”€β”€ apps.py
6
β”‚Β Β  β”‚Β Β  β”œβ”€β”€ backends.py # Handles the active session authentication
7
β”‚Β Β  β”‚Β Β  β”œβ”€β”€ migrations
8
β”‚Β Β  β”‚Β Β  β”œβ”€β”€ serializers
9
β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ login.py # Handles the proccess of login for an user
10
β”‚Β Β  β”‚Β Β  β”‚Β Β  └── register.py # Handle the creation of a new user
11
β”‚Β Β  β”‚Β Β  β”œβ”€β”€ tests.py # Test for login, registration and session
12
β”‚Β Β  β”‚Β Β  └── viewsets
13
β”‚Β Β  β”‚Β Β  β”œβ”€β”€ active_session.py # Handles session check
14
β”‚Β Β  β”‚Β Β  β”œβ”€β”€ login.py # Handles login
15
β”‚Β Β  β”‚Β Β  β”œβ”€β”€ logout.py # Handles logout
16
β”‚Β Β  β”‚Β Β  └── register.py # Handles registration
17
| |
18
β”‚Β Β  β”œβ”€β”€ fixtures # Package containg the project fixtures
19
β”‚Β Β  β”œβ”€β”€ __init__.py
20
β”‚Β Β  β”œβ”€β”€ routers.py # Define api routes
21
β”‚Β Β  └── user # Implements user app logic
22
β”‚Β Β  β”œβ”€β”€ apps.py
23
β”‚Β Β  β”œβ”€β”€ __init__.py
24
β”‚Β Β  β”œβ”€β”€ migrations
25
β”‚Β Β  β”œβ”€β”€ serializers.py # Handle the serialization of user object
26
β”‚Β Β  └── viewsets.py # Handles the modification of an user
27
|
28
β”œβ”€β”€ core # Implements app logic
29
β”‚Β Β  β”œβ”€β”€ asgi.py
30
β”‚Β Β  β”œβ”€β”€ __init__.py
31
β”‚Β Β  β”œβ”€β”€ settings.py # Django app bootstrapper
32
β”‚Β Β  β”œβ”€β”€ test_runner.py # Custom test runner
33
β”‚Β Β  β”œβ”€β”€ urls.py
34
β”‚Β Β  └── wsgi.py
35
|
36
β”œβ”€β”€ docker-compose.yml
37
β”œβ”€β”€ Dockerfile
38
β”œβ”€β”€ .env # Inject Configuration via Environment
39
β”œβ”€β”€ manage.py # Starts the app
40
└── requirements.txt # Contains development packages
Copied!

Used Patterns

Working with Django Rest Framework, the most common design pattern is the Template Method Pattern.
It mostly consists of providing base/skeleton for some features with the possibility to override/extends these skeletons.
For example, you can check the code in api/user/viewsets.py. The UserViewSet inherits of viewsets.GenericsViewSet and CreateModelMixin and UpdateModelMixin.
The UpdateModelMixin provides the logic to update an object using PUT.
We only need to rewrite the method which handles the updating and provides the serializer_class and the permission_classes.

How to use the API

POSTMAN usage
The API is actually built around these endpoints :
    api/users/signup
    api/users/login
    api/users/edit
    api/users/checkSession
    api/users/logout
Register - api/users/register
1
POST api/users/register
2
Content-Type: application/json
3
​
4
{
5
"username":"test",
6
"password":"pass",
7
8
}
Copied!
Response :
1
{
2
"success": true,
3
"userID": 1,
4
"msg": "The user was successfully registered"
5
}
Copied!
Login - api/users/login
1
cd api && django-admin startapp transactions
Copied!
Once it's done, rewrite the apps.py file with the following content.
1
​
Copied!
1
POST /api/users/login
2
Content-Type: application/json
3
​
4
{
5
"password":"pass",
6
7
}
Copied!
Response :
1
{
2
"success": true,
3
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjI4NzgxNTY4fQ.mB_YcZxZJd37r_4NYnLYeCByEHKGBC2ob0xe9KgcOII",
4
"user": {
5
"_id": 1,
6
"username": "test",
7
"email": "[email protected]"
8
}
9
}
Copied!
Logout - api/users/logout
1
POST api/users/logout
2
Content-Type: application/json
3
authorization: JWT_TOKEN (returned by Login request)
4
​
5
{
6
"token":"JWT_TOKEN"
7
}
Copied!
Response :
1
{
2
"success": true,
3
"msg": "Token revoked"
4
}
Copied!
cURL usage
Let's edit information about the user and check a session using cURL.
Check Session- api/users/checkSession
1
curl --request POST \
2
--url http://127.0.0.1:8000/api/users/checkSession \
3
--header 'Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjI4NzgxNTY4fQ.mB_YcZxZJd37r_4NYnLYeCByEHKGBC2ob0xe9KgcOII' \
4
--header 'Content-Type: application/json'
Copied!
Response :
1
{
2
"success": true
3
}
Copied!
Edit User - api/users/edit
1
curl --request POST \
2
--url http://127.0.0.1:8000/api/users/edit \
3
--header 'Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjI4NzgxNTY4fQ.mB_YcZxZJd37r_4NYnLYeCByEHKGBC2ob0xe9KgcOII' \
4
--header 'Content-Type: application/json' \
5
--data '{
6
"username": "tester",
7
"email": "[email protected]",
8
"userID": 1
9
}'
Copied!
Response :
1
{
2
"success": true
3
}
Copied!

How to extend API

Add a new model - transactions
To add a model for transaction in the project, let's create a new application in the api directory.
    Creating the app using django-admin command in the api directory.
    Then modify the name and the label of the app in apps.py
    And add the app in the INSTALLED_APPS in the settings.py of the project.
1
cd api && django-admin startapp transaction
Copied!
Then modify the apps.py file.
1
# api/transaction/apps.py
2
​
3
from django.apps import AppConfig
4
​
5
​
6
class TransactionConfig(AppConfig):
7
default_auto_field = 'django.db.models.BigAutoField'
8
name = 'api.transaction'
9
label = 'api_transaction'
Copied!
And don't forget to add the default_app_config in the __init__.py file the transaction directory.
1
# api/transaction/__init__.py
2
​
3
default_app_config = "api.transaction.apps.TransactionConfig"
Copied!
We can now register the application in settings.py file.
1
# core/settings.py
2
​
3
INSTALLED_APPS = [
4
...
5
"api.authentication",
6
"api.transaction"
7
]
Copied!
Add API interface to manage transactions
Creating an API interface to manage transactions usually go this way :
    Creating the model
    Creating the serializer
    Write the views or the viewsets
    Register the viewsets by creating routes
We've already created the model for transaction.
Let's create the serializer.
A serializer allows us to convert complex Django complex data structures such as querysets or model instances in Python native objects that can be easily converted JSON/XML format, but a serializer also serializes JSON/XML to naive Python.
1
# api/transaction/serializers.py
2
​
3
from api.transaction.models import Transaction
4
from rest_framework import serializers
5
​
6
​
7
class TransactionSerializer(serializers.ModelSerializer):
8
info = serializers.CharField(allow_null=True, allow_blank=True)
9
price = serializers.IntegerField(required=True)
10
​
11
class Meta:
12
model = Transaction
13
fields = ["id", "product", "price", "qty", "discount", "info"]
14
read_only_field = ["id", "created"]
Copied!
And now the viewsets.
The routes for the transaction interface API should look like this :
    api/transactions/create -> create transaction
    api/transactions/edit/id-> edit transaction
    api/transactions/delete/id -> delete transaction
    api/transactions/get/id -> get specific transaction
    api/transactions/get -> get all transactions
The ViewSet class comes with built-in actions :
    list
    retrieve
    create
    update
    partial_update
    destroy
And to make sure the names of the URLs match what we need, we'll be using actions.
First of all, create a file name viewsets in the transactions directory.
And add the following code.
1
# api/transactions/viewsets.py
2
​
3
from rest_framework import viewsets, status
4
from rest_framework.response import Response
5
from rest_framework.exceptions import ValidationError, NotFound, MethodNotAllowed
6
from rest_framework.decorators import action
7
from rest_framework.permissions import IsAuthenticated
8
​
9
from django.core.exceptions import ObjectDoesNotExist
10
​
11
from api.transaction.models import Transaction
12
from api.transaction.serializers import TransactionSerializer
13
​
14
​
15
class TransactionViewSet(viewsets.ModelViewSet):
16
serializer_class = TransactionSerializer
17
permission_classes = (IsAuthenticated,)
18
http_method_names = ['get', 'post']
19
20
not_found_message = {"success": False, "msg": "This object doesn't exist."}
21
missing_id_message = {"success": False, "msg": "Provide a transaction id."}
Copied!
Then let's rewrite the get_queryset method. This method is used by the viewset to return a list of objects, here a list of transactions.
1
class TransactionViewSet(viewsets.ModelViewSet):
2
...
3
​
4
def get_queryset(self):
5
return Transaction.objects.all()
Copied!
Great. Now, let's make sure DRF will exactly match the URLs we want. First of all, we have to block the default routes.
1
class TransactionViewSet(viewsets.ModelViewSet):
2
...
3
​
4
def list(self, request, *args, **kwargs):
5
raise MethodNotAllowed('GET')
6
​
7
def create(self, request, *args, **kwargs):
8
raise MethodNotAllowed('POST')
9
​
10
def destroy(self, request, *args, **kwargs):
11
raise MethodNotAllowed('DELETE')
12
​
13
def update(self, request, *args, **kwargs):
14
raise MethodNotAllowed('PUT')
15
​
16
def retrieve(self, request, *args, **kwargs):
17
raise MethodNotAllowed('GET')
Copied!
And we can write our own actions now.
Let's start with api/transactions/create.
1
class TransactionViewSet(viewsets.ModelViewSet):
2
...
3
​
4
@action(methods=['post'], detail=False, url_path='create')
5
def create_transaction(self, request, *args, **kwargs):
6
data = request.data
7
​
8
serializer = self.serializer_class(data=data)
9
serializer.is_valid(raise_exception=True)
10
serializer.save()
11
​
12
return Response({
13
"success": True
14
}, status.HTTP_201_CREATED)
Copied!
To avoid name collision with the default built-in method create , we are naming the method create_transaction. Hopefully, DRF provides the option to specify the url_path of the method.
Let's write the actions for api/transactions/get and api/transactions/get/id
1
class TransactionViewSet(viewsets.ModelViewSet):
2
...
3
4
@action(methods=['get'], detail=False, url_path='get')
5
def get_transactions(self, request, *args, **kwargs):
6
​
7
transactions = self.get_queryset()
8
​
9
page = self.paginate_queryset(transactions)
10
​
11
if page is not None:
12
serializer = self.get_serializer(page, many=True, context=self.get_serializer_context())
13
return self.get_paginated_response(serializer.data)
14
​
15
serializer = self.get_serializer(transactions, many=True, context=self.get_serializer_context())
16
return Response({
17
"success": True,
18
"transactions": serializer.data
19
}, status=status.HTTP_200_OK)
20
​
21
@action(methods=['get'], detail=False, url_path=r'get/(?P<transaction_id>\w+)')
22
23
def get_transaction(self, request, transaction_id=None, *args, **kwargs):
24
​
25
if transaction_id is None:
26
raise ValidationError(self.missing_id_message)
27
​
28
try:
29
obj = Transaction.objects.get(pk=transaction_id)
30
except ObjectDoesNotExist:
31
raise NotFound(self.not_found_message)
32
​
33
serializer = self.get_serializer(obj)
34
​
35
return Response({
36
"success": True,
37
"transaction": serializer.data
38
}, status=status.HTTP_200_OK)
Copied!
Notice that for the get/id (get_transaction), we are writing the url_path using regex expression.
And finally, the actions for api/transactions/delete/id and api/transactions/edit.
1
class TransactionViewSet(viewsets.ModelViewSet):
2
...
3
@action(methods=['post'], detail=False, url_path=r'edit/(?P<transaction_id>\w+)')
4
def edit_transaction(self, request, transaction_id=None, *args, **kwargs):
5
​
6
if transaction_id is None:
7
raise ValidationError(self.missing_id_message)
8
​
9
try:
10
obj = Transaction.objects.get(pk=transaction_id)
11
except ObjectDoesNotExist:
12
raise NotFound(self.not_found_message)
13
​
14
serializer = self.get_serializer(obj, data=request.data, partial=True)
15
serializer.is_valid(raise_exception=True)
16
self.perform_update(serializer)
17
​
18
if getattr(obj, "_prefetched_objects_cache", None):
19
obj._prefetched_objects_cache = {}
20
​
21
return Response({
22
"success": True
23
}, status.HTTP_200_OK)
24
​
25
@action(methods=['post'], detail=False, url_path=r'delete/(?P<transaction_id>\w+)')
26
def delete_transaction(self, request, transaction_id=None, *args, **kwargs):
27
​
28
if transaction_id is None:
29
raise ValidationError(self.missing_id_message)
30
​
31
try:
32
obj = Transaction.objects.get(pk=transaction_id)
33
except ObjectDoesNotExist:
34
raise NotFound(self.not_found_message)
35
​
36
obj.delete()
37
​
38
return Response({
39
"success": True
40
}, status.HTTP_200_OK)
Copied!
Now, we can register the viewset.
There is already a routers.py file which contains the routes for api/users.
Let's create a different router for transactions.
In the api directory, create a new package called routers. Move the file routers.py in it and rename it to users.py.
Then create a new file named transactions.py and add the following code to register TransactionViewSet actions.
1
# api/routers/transactions.py
2
​
3
from rest_framework import routers
4
​
5
from api.transaction.viewsets import TransactionViewSet
6
​
7
router = routers.SimpleRouter(trailing_slash=False)
8
​
9
router.register('', TransactionViewSet, basename='transactions')
10
​
11
urlpatterns = [
12
*router.urls,
13
]
Copied!
And the last step, open the urls.py file in the core directory and add the transaction router.
1
# core/urls.py
2
​
3
from django.urls import path, include
4
​
5
urlpatterns = [
6
path("api/users/", include(("api.routers.users", "api-users"), namespace="api-users")),
7
path("api/transactions/", include(("api.routers.transactions", "api-transactions"), namespace="api-transactions"))
8
]
Copied!
And that's it. You can start testing the API with Postman.
Congratulations. You just learned :
    How to add a new model;
    How to add a serializer for this model;
    How to rewrite viewset behavior and actions to match your needs.
Last modified 2mo ago