There are many ways to structure a Django project. Finding the design that ensures maintainability and scalability is difficult. This is my objective.
I’ve been studying Domain Driven Design and how to incorporate it in Django with Django REST Framework. I want to design a robust architecture that's easy for documentation, and effective for both collaboration and solo projects. Whether I'll achieve this or not, this endeavor will be an important experience for me.
The project is still ongoing and there are topics in DDD that I have not fully explored. I'm going to do my best to study them, and hopefully, release an initial stable version of this by the end of this year. Without further ado, let's begin.
Overview
This is the initial structure:
project/
api/
migrations/
services/
blogs/
read_blog.py
__init__.py
apps.py
project/
__init__.py
asgi.py
settings.py
urls.py
wsgi.py
resources/
migrations/
repositories/
base_repository.py
blog_repository.py
serializers/
blog_serializer.py
__init__.py
admin.py
apps.py
models.py
Right now, it has three applications: api, project, and resources. Make sure to create them and install the djangorestframework
and django-cors-headers
packages.
Project
The project folder is our core application. The folder name is based on the name you enter for your django project, so you don't have to follow the name provided in this example. By default, this is where the settings, wsgi, asgi, and urls are stored. We can also store other files here such as classes for JWT, custom middlewares, and so on. Since we're using other packages, we need to configure our settings file:
project/settings.py
INSTALLED_APPS = [
…
'corsheaders',
'rest_framework',
'api.apps.ApiConfig',
'resources.apps.ResourcesConfig',
]
MIDDLEWARE = [
…
'corsheaders.middleware.CorsMiddleware',
…
]
# this is set to true for now
CORS_ORIGIN_ALLOW_ALL = True
We'll revisit this folder later once we establish our api services.
Resources
The resources folder is where we place our models, repositories, and serializers. Our models will contain the schema for our database tables, while our repositories will handle fetching of objects from the persistence layer (ORM). Lastly, the serializers will render our querysets into JSON.
resources/models.py
from django.contrib.auth.models import User
from django.db import models
class Blog(models.Model):
content = models.TextField()
summary = models.CharField(max_length=100)
title = models.CharField(max_length=100)
created_on = models.DateTimeField(auto_now_add=True)
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
default=None
)
def __str__(self):
return self.title
For our repositories, we need a base repository file where all base methods are registered and can be inherited by different model repositories. Each model repository can have their own methods for aggregate and complex queries:
resources/repositories/base_repository.py
from django.db import InternalError, DatabaseError
from django.http import Http404, HttpResponseServerError
class BaseRepository(object):
def find(self, id, value_list=()):
try:
if len(value_list):
return self.model.objects.get(pk=id).values(*value_list)
else:
return self.model.objects.get(pk=id)
except self.model.DoesNotExist:
raise Http404
except InternalError:
raise HttpResponseServerError
except DatabaseError:
raise Http404
resources/repositories/blog_repository.rb
from resources.models import Blog
from resources.repositories.base_repository import BaseRepository
class BlogRepository(BaseRepository):
def __init__(self):
self.model = Blog
resources/serializers/blog_serializer.py
from rest_framework import serializers
from resources.models import Blog
class BlogSerializer(serializers.ModelSerializer):
class Meta:
model = Blog
fields = [
'id',
'content'
'summary',
'title',
'created_on'
]
You may be wondering what's the importance of the repository since we already have the ORM. One reason is that this allows the project to migrate to different databases with ease, especially if the ORM methods vary from the default ORM or if the database has no ORM package for Django. When this happens, we only need to update the queries in the repository without touching the services in our api. Here's an example:
From RDBMS
ModelServices | ModelRepository
ModelRepository().find(id) | Model.objects.get(pk=id)
To NoSQL
ModelServices | ModelRepository
ModelRepository().find(id) | Model.find_one({"_id": id})
API
The api is where all the class views for our endpoints are stored. For now, it only has the services folder where sub-folders based on the models are stored, and each sub-folder has files dedicated for each CRUD operation. In this example, we have a blogs folder and a read blog file for viewing specific blog record:
api/services/blogs/read_blog.py
from rest_framework.views import APIView
from rest_framework.response import Response
from resources.repositories.blog_repository import BlogRepository
from resources.serializers.blog_serializer import BlogSerializer
class ReadBlogService(APIView):
def get(self, request, id, format=None):
blog = BlogRepository().find(id)
return Response(BlogSerializer(blog, many=False).data)
Final adjustments
Now, we can edit the project.urls file to register the new blog service:
project/urls.py
from django.contrib import admin
from django.urls import path
from api.services.blogs.read_blog import ReadBlogService
urlpatterns = [
path('admin/', admin.site.urls),
path('blog/<int:id>', ReadBlogService.as_view()),
]
Conclusion
This is what I've conceptualized so far. It may seem complicated but that's because it's meant for medium-scale and large-scale projects. It won't be useful for small applications. Of course, I don't expect this to be the ideal choice for Django programmers. As I've mentioned before, there are many ways to structure a Django project. There are undoubtedly other designs out there that are better and battle-tested. But I'll do my best to make this a potential option.
What are your thoughts on this?
Discussion (0)