π§ Why?
- Because after 7 years working on the same big Django codebase, when I wanted to start a new project from scratch (a small demo for another blog post thatβs yet to come π ), I had a hard time
- Because none of the Django project templates I found on the web felt right to me (either too opinionated, or too weak on some aspects)
- Because tooling has evolved greatly recently
One of the reasons Django is awesome is because itβs unopinionated: it lets you make your own choices. But sometimes itβs intimidating. The idea here is to be opinionated on whatβs around Django (Python tooling, structure, environment, UI tooling) but let you make your own implementation choices when it comes to your application.
π€ TL;DR
This blog post introduces a Django project template Iβve been working on in the past few weeks. You can find it here and try it by yourself (and you can also find an example project created using the template ), but as itβs a bit unusual, you may want to read this.
βοΈ A quick word on tooling
π Python dependency management: uv, what else?
Iβve been a long-time pip-tools + native venv (+ sometimes pyenv ) user. When pipenv , then poetry , then pdm came, some people tried to tell me that I should switch. I was never convinced. I was always doubtful about an all-in-one tool pretending to replace my custom-tailored assembly of well established specialized tools, I didnβt see enough value.
But then came uv . It made me realize that no matter my theoretical doubts, if a tool a just 100x faster, I had to embrace it.
So donβt be surprised to find this Django project template using uv.
βοΈ Task runner: just use just
Iβve been a long-time Makefile user. But I was frustrated by the .PHONY thing, and by the fact that some behaviors seemed weird to me (because I was hacking a build tool to make a task runner out of it).
just
is what I needed from the beginning, and it doesnβt just avoid Makefileβs caveats, it also offers other advantages. So Iβve used it in this template.
ποΈ Project structure
The default project structure proposed by the official Django tutorials always felt odd to me. At the end of step 1 , you end up with:
.
βββ mysite
β βββ manage.py
β βββ mysite
β β βββ asgi.py
β β βββ __init__.py
β β βββ settings.py
β β βββ urls.py
β β βββ wsgi.py
β βββ polls
β βββ admin.py
β βββ apps.py
β βββ __init__.py
β βββ migrations
β β βββ __init__.py
β βββ models.py
β βββ tests.py
β βββ views.py
βββ pyproject.toml
What I donβt like about it (ordered by -criticity):
- Having my Django apps nested under my Django project: not only it doesnβt help me thinking about my apps as potentially reusable (ensuring separation of concerns), but also it creates a situation where the configuration folder (
mysite) will be lost in the middle of apps folders. I would rather have anappsfolder where I would put all my apps, wouldnβt you? - Having a redundant
mysitenaming (one for my Git root, one for my Django project): not only it feels weird to typemysite/mysitewhen I need to edit my site configuration, but also the Django project itself will mostly contain configuration, so I would rather name itconf - Not having
manage.pyat my Git root - Not having
wsgi.py/asgi.pyat the same level asmanage.py, while their job is essentially the same (bootstrap the application and load the configuration, just for different interfaces)
It may seem like minor concerns, but when you work with a codebase on a daily basis, some of them can feel annoying.
Here is the proposed structure:
.
βββ apps
β βββ polls
β βββ admin.py
β βββ apps.py
β βββ __init__.py
β βββ migrations
β β βββ __init__.py
β βββ models.py
β βββ tests.py
β βββ views.py
βββ conf
β βββ settings.py
β βββ urls.py
βββ asgi.py
βββ manage.py
βββ pyproject.toml
βββ wsgi.py
With this, anyone can immediately tell that my project is a collection of apps assembled with some configuration, and anyone immediately knows where to find anything.
Fortunately, Django is awesome: native commands startproject and startapp take arguments! I just had to write some recipes to get what I wanted. The only thing that I couldnβt get to work is having asgi.py and wsgi.py at the same level as manage.py, but that was my least critical issue.
π‘ In case you wonder how itβs done:
- As said in the message of the commit creating the template structure, the command run was
uv run manage.py startproject conf .- The
justrecipe to start a Django app in this structure can be seen here
π§± Configuration
This is one of the main pain points Iβve seen in web development in general, and using vanilla Django doesnβt help as much as it should/could, at least when the project grows. We have our conf/settings.py file, itβs fine. But what happens when we add many third-party apps with their own settings, and when we write many home-made apps, as itβs usually done as soon as Django is used beyond a simple blog? (and please, use Django for way more than a simple blog, itβs awesome! β¨). Here are the key issues:
- How do we split our settings to keep them readable? Is it gonna be django-split-settings ? Do we need a third-party lib for that?
- How do we separate environments (devel/testing/staging/prod)? Is it gonna be django-configurations ? Do we need a third-party lib for that?
- How do we even manage environment variables? Is it gonna be
.envfiles? If so, what about secrets? And how are we going to load envvars into Django? Is it gonna be environs ? python-dotenv ? I thought loading envvars into an application was the environments job, not the applicationsβ¦
But what if I told you you can solve all this without any Python dependency?
π· Django settings
π¨οΈ A place for everything and everything in its place
Consider this structure:
.
βββ apps
β βββ β¦
β βββ β¦
βββ conf
β βββ β¦
β βββ settings
β β βββ apps
β β β βββ __init__.py
β β β βββ myapp.py
β β βββ base.py
β β βββ default.py
β β βββ devel.py
β β βββ __init__.py
β β βββ testing.py
β βββ β¦
Here is how it works:
settings/base.pyis basically thesettings.pythatstartprojectcommand generateddefault.pyimports everything frombase.py, and everything from every Python module found insettings/apps; itβs calleddefaultbecause everything it contains is imported by__init__.py; this makesconf.settingsa perfectly valid Django settings module, allowing defaultmanage.py/wsgi.py/asgi.pyto work out-of-the-box- Whenever you use the
just startapp myapprecipe, amyapp.pyis created withinsettings/apps - Whenever you install a third-party app, you can put its settings in
settings/apps/THE_APP.pyin order to find them easily later
- Whenever you use the
devel.pyandtesting.pyimport everything fromdefault.pyand exist in order to override things for development or testing only ; itβs usually small files with a few overrides
With this simple Python package/module/import structure, leveraging nothing else than Pythons awesomeness, Iβve found myself able to easily manage settings of dozens of Django apps (both third-party and home-made) and multiple environments (devel/testing/staging/prod) without any friction:
- Each settings file stays thin, even
base.py - Each apps settings file can insert stuff into
MIDDLEWAREby simplyfrom ..base import MIDDLEWARE - Environment-specific overrides can be found immediately
π€ You might think that this settings structure is too much for many Django projects. And you would be right! This Django project template doesnβt target small Django applications: it proposes solutions that have proven to be effective when working on a Django project that went big (~15 third-party apps + ~15 home-made apps).
ποΈ Environment variables and secrets
Environment variables (envvars) are the right way to load environment-specific settings into an application, we know that for some time now. The .env non-standard has become a de-facto standard, but it comes with its own challenges:
- How to load envvars from the
.envfile into our applicationβs environment? - What about version-control? Do we distribute a generic
.env.distfile that each developer has to copy? What about the production.envfile? - As the
.envfiles are physical files, how do we manage secrets?
In my experience this has been a giant pain ; not because itβs impossible to solve, but because the solutions Iβve seen working always felt horribly over-complicated to my taste. And everybody seems to be fine with it. To be clear:
- I donβt want two separate mechanisms to manage my envvars, according to their need of secrecy
- I donβt want to ask myself βIs this a secret?β every time I add an envvar
- And hell I donβt want to be forced to depend on a cloud providers βvaultβ feature, even if the cost is near-zero
π¨οΈ (slamming the table) There must be a better way!
And there is! Beholdβ¦ SOPS . Erm, yeah I know, I know, itβs not exactly new. But I havenβt met a Django project that makes use of it yet, so I gave it a try.
In this Django project template, every environment gets a version-controlled .env file containing all its envvars, including the secrets!
See how easy it is to edit an encrypted .env file using this small PyCharm plugin
:
I also chose to put all .env files in a dedicated directory named⦠envs, with a dedicated subdirectory for all devel environments of your team. After project initialization (just init david), you get:
.
βββ apps
β βββ β¦
βββ conf
β βββ β¦
βββ envs
β βββ devel
β β βββ david.enc.env
β βββ production.enc.env
βββ β¦
Files with extension .enc.env can safely be version-controlled, as they are encrypted by SOPS.
π‘ You can see what SOPS-encrypted
.envfiles look like in the example project based on the template here
π Secrets encryption
In order to be fully independent, I chose age
(pronounced [aΙ‘eΜ]
), the encryption tool that comes with SOPS. Future versions of this project template may include the ability of choosing your encryption method.
π€ Collaboration
As SOPS keeps .env files keys human-readable (only values are encrypted), you can see when one of your teammates adds a new envvar somewhere (but you canβt see the value if itβs in their own .env file).
I chose to generate two distinct encryption key pairs:
- One for you, i.e. for your own
.envfile - One for the team, i.e. for shared
.envfiles, such as production
Both public keys live in envvars defined in your .env file.
The secret keys live in a file within your $HOME, so you need to find a way to share the shared secret keys with your teammates. And probably you should make backups π¬.
π Deployment and decryption
SOPS comes with a handy exec-env command (see the docs
) that decrypts an encrypted .env file and exports every envvar found in it before starting as a child process the command you want it to execute. Perfect for working with Gunicorn!
Obviously the tricky part is to provision the production secret key to your production infrastructure. Methods will vary greatly, but you have an example with Flux there .
π» Working on local devel environment
Obviously on your local environment you will keep an unencrypted version of your .env file (which is Git-ignored in the template), and it will be automatically loaded if you start Django using the following command:
just runserver
Yes! just loads .env files, and I wrote the recipe to leverage uv in order to start the right Python, with the right virtualenv. Everything works out-of-the-box! You can find a list of handful commands in the templateβs README
.
π‘ Introduction of
SOPSand secrets management to the project template can be seen here .
π
βFront-endβ Web UI
Now that is an area of web development where I feel the Django ecosystem could do better. We are stuck in a place where:
- Django is awesome!
- Its templating system is both powerful and easy to use
- The built-in
staticfilesapp makes a wonderful job at collecting JS/CSS/image files from every installed app and put them into a unified storage ready to be served. It can even avoid the cache hell with the cache-busting feature of theManifestStaticFilesStorage!
- But for modern web UIs, static files are not enough:
- The CSS that CSS specialists want to write is not the one they want to send to the browsers: they want it minified, they want to use nested selectors that are not yet supported, maybe even SCSS mixins and third-party libs!
- The same is true for Javascript, but at a much higher severity: Javascript specialists want to write ES modules and classes, they want to use
importfor using third-party code, they want linting, maybe even typing! - And guess what: they are right! We simply cannot deliver static jQuery plugins like itβs 2010: web UI elements of a website cannot be considered second-class citizens!
Between 2015 and 2022, many teams have worked around this by reducing Django to an API βback-endβ, and writing a Javascript-first βfront-endβ with horrific maintenance costs. That was, in many cases, an absurd choice. Fortunately, some people are getting convinced that the web platform should (and can) follow the hypermedia principles, and more and more websites are sending HTML over the wire again.
β So how do we make web UI stuff first-class citizens with Django? I introduced this in my follow-up of the βMother of all Htmx Demosβ , but not in a detailed way. Today Iβm going a step further: itβs integrated in my proposed Django project template. Here is what it offers:
- CSS and Javascript dependency management with standard
package.jsonfor use withnpm: never import CSS/JS from CDN again, and keep your dependencies up-to-date! - Fast build/transpilation/lint/bundling/minification with esbuild
- Write your CSS and JS code as you want in each Django apps
static_srcdirectory, it gets compiled to thestaticdirectory, so that Djangoβsstaticfilesawesomeness can shine (you can use{% static %}templatetag, compiled files get collected) ManifestStaticFilesStorageenabled by default- A Django app named
uiwhere you can centralize your foundation UI stuff like base styles, components, UI kit, etc.
Basically the idea is to get the best of both worlds, in order to make it realistic to build a modern website or webapp with Django, and make web UI specialists want to work with Django!
π‘ How it works is visible here .
π‘ A typical integration of the famous Bootstrap UI framework can be seen in the example project based on the template, here . Yes itβs a bit more verbose than just downloading Bootstrap from a CDN, but itβs the cost of getting things done the right way.
β So what now?
This project template is at very early stage. Some things are missing, others are imperfect. But I hope you get the idea, and I hope that the idea makes sense to you. Feedback is more than welcome, because:
- If itβs considered useless, Iβll stop working on it
- If itβs considered useful but flawed, I may take some time to improve it
- If itβs considered useful but just imperfect/incomplete, I will take some time to fix bugs and add features
π Legal notice
The repository is released under GPLv3 license, which means:
- You are allowed to use this template for any kind of project, unconditionally
- If you modify this template or include it as part of a larger project template, you have to release your changes and your derived work under GPLv3
