This sample explains how to extend an existing Django API Starter and add another model managed via a new API node.
Codebase structure
Copy PROJECT ROOT
├── api # App containing all project-specific apps
│ ├── apps.py
│ ├── authentication # Implements authentication app logic (register, login, session) logic
│ │ ├── apps.py
│ │ ├── backends.py # Handles the active session authentication
│ │ ├── migrations
│ │ ├── serializers
│ │ │ ├── login.py # Handles the proccess of login for an user
│ │ │ └── register.py # Handle the creation of a new user
│ │ ├── tests.py # Test for login, registration and session
│ │ └── viewsets
│ │ ├── active_session.py # Handles session check
│ │ ├── login.py # Handles login
│ │ ├── logout.py # Handles logout
│ │ └── register.py # Handles registration
| |
│ ├── fixtures # Package containg the project fixtures
│ ├── __init__.py
│ ├── routers.py # Define api routes
│ └── user # Implements user app logic
│ ├── apps.py
│ ├── __init__.py
│ ├── migrations
│ ├── serializers.py # Handle the serialization of user object
│ └── viewsets.py # Handles the modification of an user
|
├── core # Implements app logic
│ ├── asgi.py
│ ├── __init__.py
│ ├── settings.py # Django app bootstrapper
│ ├── test_runner.py # Custom test runner
│ ├── urls.py
│ └── wsgi.py
|
├── docker-compose.yml
├── Dockerfile
├── .env # Inject Configuration via Environment
├── manage.py # Starts the app
└── requirements.txt # Contains development packages
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 :
Register - api/users/register
Copy POST api / users / register
Content - Type : application / json
{
"username" : "test" ,
"password" : "pass" ,
"email" : "test@appseed.us"
}
Response :
Copy {
"success" : true ,
"userID" : 1 ,
"msg" : "The user was successfully registered"
}
Login - api/users/login
Copy cd api && django-admin startapp transactions
Once it's done, rewrite the apps.py
file with the following content.
Copy POST / api / users / login
Content - Type : application / json
{
"password" : "pass" ,
"email" : "test@appseed.us"
}
Response :
Copy {
"success" : true ,
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjI4NzgxNTY4fQ.mB_YcZxZJd37r_4NYnLYeCByEHKGBC2ob0xe9KgcOII",
"user" : {
"_id" : 1 ,
"username" : "test" ,
"email" : "test@appseed.us"
}
}
Logout - api/users/logout
Copy POST api / users / logout
Content - Type : application / json
authorization : JWT_TOKEN (returned by Login request)
{
"token" : "JWT_TOKEN"
}
Response :
Copy {
"success" : true ,
"msg" : "Token revoked"
}
cURL usage
Let's edit information about the user and check a session using cURL.
Check Session - api/users/checkSession
Copy curl --request POST \
--url http://127.0.0.1:8000/api/users/checkSession \
--header 'Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjI4NzgxNTY4fQ.mB_YcZxZJd37r_4NYnLYeCByEHKGBC2ob0xe9KgcOII' \
--header 'Content-Type: application/json'
Response :
Edit User - api/users/edit
Copy curl --request POST \
--url http://127.0.0.1:8000/api/users/edit \
--header 'Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjI4NzgxNTY4fQ.mB_YcZxZJd37r_4NYnLYeCByEHKGBC2ob0xe9KgcOII' \
--header 'Content-Type: application/json' \
--data '{
"username": "tester",
"email": "test@admin.us",
"userID": 1
}'
Response :
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.
Copy cd api && django-admin startapp transaction
Then modify the apps.py
file.
Copy # api/transaction/apps.py
from django . apps import AppConfig
class TransactionConfig ( AppConfig ):
default_auto_field = 'django.db.models.BigAutoField'
name = 'api.transaction'
label = 'api_transaction'
And don't forget to add the default_app_config
in the __init__.py
file the transaction
directory.
Copy # api/transaction/__init__.py
default_app_config = "api.transaction.apps.TransactionConfig"
We can now register the application in settings.py
file.
Copy # core/settings.py
INSTALLED_APPS = [
...
"api.authentication" ,
"api.transaction"
]
Add API interface to manage transactions
Creating an API interface to manage transactions usually go this way :
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.
Copy # api/transaction/serializers.py
from api . transaction . models import Transaction
from rest_framework import serializers
class TransactionSerializer ( serializers . ModelSerializer ):
info = serializers . CharField (allow_null = True , allow_blank = True )
price = serializers . IntegerField (required = True )
class Meta :
model = Transaction
fields = [ "id" , "product" , "price" , "qty" , "discount" , "info" ]
read_only_field = [ "id" , "created" ]
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 :
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.
Copy # api/transactions/viewsets.py
from rest_framework import viewsets , status
from rest_framework . response import Response
from rest_framework . exceptions import ValidationError , NotFound , MethodNotAllowed
from rest_framework . decorators import action
from rest_framework . permissions import IsAuthenticated
from django . core . exceptions import ObjectDoesNotExist
from api . transaction . models import Transaction
from api . transaction . serializers import TransactionSerializer
class TransactionViewSet ( viewsets . ModelViewSet ):
serializer_class = TransactionSerializer
permission_classes = (IsAuthenticated , )
http_method_names = [ 'get' , 'post' ]
not_found_message = { "success" : False , "msg" : "This object doesn't exist." }
missing_id_message = { "success" : False , "msg" : "Provide a transaction id." }
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.
Copy class TransactionViewSet ( viewsets . ModelViewSet ):
...
def get_queryset ( self ):
return Transaction . objects . all ()
Great. Now, let's make sure DRF will exactly match the URLs we want. First of all, we have to block the default routes.
Copy class TransactionViewSet ( viewsets . ModelViewSet ):
...
def list ( self , request , * args , ** kwargs ):
raise MethodNotAllowed ( 'GET' )
def create ( self , request , * args , ** kwargs ):
raise MethodNotAllowed ( 'POST' )
def destroy ( self , request , * args , ** kwargs ):
raise MethodNotAllowed ( 'DELETE' )
def update ( self , request , * args , ** kwargs ):
raise MethodNotAllowed ( 'PUT' )
def retrieve ( self , request , * args , ** kwargs ):
raise MethodNotAllowed ( 'GET' )
And we can write our own actions now.
Let's start with api/transactions/create
.
Copy class TransactionViewSet ( viewsets . ModelViewSet ):
...
@action (methods = [ 'post' ], detail = False , url_path = 'create' )
def create_transaction ( self , request , * args , ** kwargs ):
data = request . data
serializer = self . serializer_class (data = data)
serializer . is_valid (raise_exception = True )
serializer . save ()
return Response ({
"success" : True
}, status.HTTP_201_CREATED)
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
Copy class TransactionViewSet ( viewsets . ModelViewSet ):
...
@action (methods = [ 'get' ], detail = False , url_path = 'get' )
def get_transactions ( self , request , * args , ** kwargs ):
transactions = self . get_queryset ()
page = self . paginate_queryset (transactions)
if page is not None :
serializer = self . get_serializer (page, many = True , context = self. get_serializer_context ())
return self . get_paginated_response (serializer.data)
serializer = self . get_serializer (transactions, many = True , context = self. get_serializer_context ())
return Response ({
"success" : True ,
"transactions" : serializer.data
}, status = status.HTTP_200_OK)
@action (methods = [ 'get' ], detail = False , url_path = r 'get/ ( ?P<transaction_id> \w + ) ' )
def get_transaction ( self , request , transaction_id = None , * args , ** kwargs ):
if transaction_id is None :
raise ValidationError (self.missing_id_message)
try :
obj = Transaction . objects . get (pk = transaction_id)
except ObjectDoesNotExist :
raise NotFound (self.not_found_message)
serializer = self . get_serializer (obj)
return Response ({
"success" : True ,
"transaction" : serializer.data
}, status = status.HTTP_200_OK)
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
.
Copy class TransactionViewSet ( viewsets . ModelViewSet ):
...
@action (methods = [ 'post' ], detail = False , url_path = r 'edit/ ( ?P<transaction_id> \w + ) ' )
def edit_transaction ( self , request , transaction_id = None , * args , ** kwargs ):
if transaction_id is None :
raise ValidationError (self.missing_id_message)
try :
obj = Transaction . objects . get (pk = transaction_id)
except ObjectDoesNotExist :
raise NotFound (self.not_found_message)
serializer = self . get_serializer (obj, data = request.data, partial = True )
serializer . is_valid (raise_exception = True )
self . perform_update (serializer)
if getattr (obj, "_prefetched_objects_cache" , None ):
obj . _prefetched_objects_cache = {}
return Response ({
"success" : True
}, status.HTTP_200_OK)
@action (methods = [ 'post' ], detail = False , url_path = r 'delete/ ( ?P<transaction_id> \w + ) ' )
def delete_transaction ( self , request , transaction_id = None , * args , ** kwargs ):
if transaction_id is None :
raise ValidationError (self.missing_id_message)
try :
obj = Transaction . objects . get (pk = transaction_id)
except ObjectDoesNotExist :
raise NotFound (self.not_found_message)
obj . delete ()
return Response ({
"success" : True
}, status.HTTP_200_OK)
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.
Copy # api/routers/transactions.py
from rest_framework import routers
from api . transaction . viewsets import TransactionViewSet
router = routers . SimpleRouter (trailing_slash = False )
router . register ( '' , TransactionViewSet, basename = 'transactions' )
urlpatterns = [
* router . urls ,
]
And the last step, open the urls.py file in the core directory and add the transaction router.
Copy # core/urls.py
from django . urls import path , include
urlpatterns = [
path ( "api/users/" , include (( "api.routers.users" , "api-users" ), namespace = "api-users" )),
path ( "api/transactions/" , include (( "api.routers.transactions" , "api-transactions" ), namespace = "api-transactions" ))
]
And that's it. You can start testing the API with Postman.
Congratulations. You just learned :
How to add a serializer for this model;
How to rewrite viewset
behavior and actions
to match your needs.