Multitenant Systems - Part II - Building multitenant backend APIs using Django

Search for a command to run...

No comments yet. Be the first to comment.
Developers often tell me that the requirements they receive are usually vague and unclear. Another common complaint is that the requirements change midway, causing them to get stuck. Additionally, sometimes the deadline is set before the requirements...

Imagine you've got a REST API for listing users, and each user's information shows up in 12 fields. GET /v1/users/?page=1&page_size=15 { "pagination_data": { "total_records": 10000, "pages": 667, "prev_page_no": null, ...

Decoupling the Data Layer for Enhanced Code Modularity

Essentials for Safeguarding and Maintaining Your Sprint Cycles

A Step-by-Step Tutorial with Practical Examples

This is a continuation of part I of the multitenant series. You can check part 1 here. In this one, I will discuss how we can build the backend of our app using the third architecture discussed in the part I.
An online shop app! This will be a multitenant app where a user can create their own online shop and add users and products to this shop. There can exist any number of shops in our app.

A user can create a new shop and visit their shop using the shop code as the subdomain. Let's build the backend APIs for this system using the Django REST Framework.
We will have a table Tenant to store all the shops. All the other tables will have a reference to the Tenant table.

import uuid
from django.db import models
from django.contrib.auth.models import AbstractBaseUser
from .managers.tenant import TenantManager
from .managers.user import CustomUserManager
class Tenant(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=128)
code = models.CharField(max_length=128, unique=True)
class TenantBaseModel(models.Model):
class Meta:
abstract = True
objects = TenantManager()
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE)
class User(AbstractBaseUser, TenantBaseModel):
USERNAME_FIELD = 'id'
REQUIRED_FIELDS = list()
user_objects = CustomUserManager()
first_name = models.CharField(max_length=512)
last_name = models.CharField(max_length=512, null=True, blank=True)
email = models.EmailField(max_length=512)
class Meta:
unique_together = ('tenant_id', 'email',)
class Product(TenantBaseModel):
name = models.CharField(max_length=256)
code = models.CharField(max_length=256)
added_by = models.ForeignKey(User, on_delete=models.CASCADE)
added_on = models.DateTimeField(auto_now_add=True)
TheTenant model on the top stores all the shop information.
The TenantBaseModel is an abstract class that can be inherited from all our tenant models like User and Product so they automatically get the tenant fields.
We have overridden the objects in the TenantBaseModel with the TenantManager which we will see next.
from django.db import models
class TenantManager(models.Manager):
def get_queryset(self):
return None
def t_filter(self, tenant, **kwargs):
return super(TenantManager, self) \
.get_queryset() \
.filter(tenant=tenant, **kwargs)
We override the get_queryset to return None.
So, AnyTenantModel.objects.all() or AnyTenantModel.objects.filter() will return None.
We can use t_filter to get the single tenant related information, AnyTenantModel.objects.t_filter(tenant=tenant).
Every tenant-based API should have a tenant code passed. We can pass the tenant code via the API headers. This middleware will pick the tenant code from the headers, get the tenant object from the DB, and set the tenant in the request object.
from django.http import Http404
from django.urls import reverse
from ..models import Tenant
TENANT_URLS = [
reverse('tenants-list')
]
class CustomTenantMiddleware:
def __init__(self, get_response):
self.get_response = get_response
self.TENANT_NOT_FOUND_EXCEPTION = Http404
def __call__(self, request):
code = request.META.get('HTTP_TENANT_CODE')
if request.path in TENANT_URLS:
return self.get_response(request)
try:
tenant = Tenant.objects.get(code=code)
request.tenant = tenant
return self.get_response(request)
except Exception:
raise self.TENANT_NOT_FOUND_EXCEPTION(
f'Tenant with code \'{code}\' does not exists'
)
Once the JWT token authentication is done, we also check if this authenticated user is a part of this tenant.
from rest_framework import exceptions
from knox.auth import TokenAuthentication
from ..models import User
class CustomTokenAuthentication(TokenAuthentication):
def authenticate(self, request):
user, auth_token = super().authenticate(request)
if user.tenant != request.tenant:
raise exceptions.AuthenticationFailed(
'User does not exist in this tenant')
return user, auth_token
Once we have completed all the above steps, we can easily integrate all this together. Our authentication class will be the CustomTokenAuthentication class, we can set it globally in the settings file as well.
We override get_queryset to use t_filter and we get the tenant in the request object. So User.objects.t_filter(tenant=self.request.tenant) means all the users in this tenant.
from rest_framework import viewsets
from ..serializers.user import UserSerializer
from ..models import User
from ..authentication.auth import CustomTokenAuthentication
class UserViewSet(viewsets.ModelViewSet):
serializer_class = UserSerializer
authentication_classes = (CustomTokenAuthentication, )
pagination_class = None
def get_queryset(self):
return User.objects \
.t_filter(tenant=self.request.tenant) \
.order_by('first_name')
from rest_framework import viewsets
from ..models import Product
from ..serializers.product import ProductSerializer
from ..authentication.auth import CustomTokenAuthentication
class ProductViewSet(viewsets.ModelViewSet):
serializer_class = ProductSerializer
authentication_classes = (CustomTokenAuthentication, )
pagination_class = None
def get_queryset(self):
return Product.objects \
.t_filter(tenant=self.request.tenant) \
.order_by('code')
Tada! You have your multitenant backend APIs ready. Check out the full code here.
Thanks for reading!