The DjangoXX Stack is a stack of boring technologies to build modern web apps. With this stack, a team of one can build, maintain and operate a SaaS with minimal friction, great user experience, and without deep frontend expertise. The stack is composed of a classic Django backend (Django with PostgreSQL and Redis if needed) with JinjaX and HTMX.
On my previous jobs, every time we have to build a "web app", we usually go for the usual stack: use React, Vue, or Angular. However, as I've been on small teams, this means that we need to hire someone with deep knowledge of these frameworks, otherwise you can get stuck or really slow down quickly.
What bothers me most is that these frameworks' benefits aren't obvious to me:
So for one of my current projects, I will not use any of the big frontend frameworks. I’m not saying “no JS”, but rather that I'll only use it when really needed.
I ended up using the following stack, that I call the DjangoXX Stack.
Django for the backend. Even if I dislike many design decisions, it is still the framework that I know the best. I go for PostgreSQL, django-q2 for asynchronous tasks, and django-allauth for auth.
Jinja2 with JinjaX to render HTML. Jinja2 and Django Templates (even version 6) are limited as component systems. JinjaX fills the gap to be able to define components as if they are standard HTML tags. You define inputs for your component and then you can use them.
Tailwind with Preline for the styling. I was not a fan of Tailwind ("it is like inline styles everywhere, right?"), but it turns out to be very convenient once you combine it with a component system.
And to manage auto-refresh, I'm using HTMX. There are now plenty of libraries similar to it, but for now (and the future?) my needs are extremely basic: do not do full page refreshes, and sometimes when my app is in some state (ex: generating a document), I need to refresh some components of the page regularly.
I have 2 "base" templates: one skeleton version for HTMX requests and one full layout for normal requests.
When receiving the request, Django gathers the data, and passes it to the rendering engine.
I created a library of JinjaX component, that I build as part of my project. It's quite easy to do, mostly copy-pasting from the preline examples or generated through an LLM.
LLMs actually handle the component library quite well, so your HTML doesn’t end up as a messy pile of divs and Tailwind classes—it looks like a clean HTML page with readable custom tags.
Everything is rendered on the server as HTML. Since HTMX works through annotations within the markup, integrating it is as simple as adding an attribute to your div.
If the request is an HTMX request (simply check the header), I just render the requested component ; sometime it is on the same endpoint (when I render a big chuck of the page), sometime it has its dedicated endpoint, when only a few components are required.
{#def
kind="solid",
dense=False,
#}
{% if dense %}
{% set density_classes = "py-1 px-2" %}
{% else %}
{% set density_classes = "py-3 px-4" %}
{% endif %}
{% if kind == "solid" %}
{% set kind_classes = "inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-blue-600 text-white hover:bg-blue-700 focus:outline-hidden focus:bg-blue-700 disabled:opacity-50 disabled:pointer-events-none" %}
{% elif kind == "outline" %}
{% set kind_classes = "inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 text-gray-500 hover:border-blue-600 hover:text-blue-600 focus:outline-hidden focus:border-blue-600 focus:text-blue-600 disabled:opacity-50 disabled:pointer-events-none" %}
{% elif kind == "ghost" %}
{% set kind_classes = "inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent text-blue-600 hover:bg-blue-100 hover:text-blue-800 focus:outline-hidden focus:bg-blue-100 focus:text-blue-800 disabled:opacity-50 disabled:pointer-events-none" %}
{% else %}
{% set kind_classes = "inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-blue-600 text-white hover:bg-blue-700 focus:outline-hidden focus:bg-blue-700 disabled:opacity-50 disabled:pointer-events-none" %}
{% endif %}
{% do attrs.add_class(kind_classes) %}
{% do attrs.add_class(density_classes) %}
{% do attrs.add_class("cursor-pointer") %}
{% do attrs.setdefault(type="button") %}
<button {{ attrs.render() }}>
{{ content }}
</button>
This stack isn’t perfect. Here are the rough edges I’ve hit so far.
I haven't found an automatic way of detecting the used Tailwind classes. So I have a dirty python script that I run to extract all things that look like a Tailwind class to make the Tailwind CLI generates the CSS file.
JinjaX's syntax differs slightly from Jinja2. Tags arguments must be strings or mustache expressions ({{ python code }}). You can't mix things like "Username: {{ user.name }}". It is annoying, it is not consistent with the rest of the code, I often forget it, and I havn't find a way to force LLM to always respect this rule.
I haven't done any performance test, I don't know if JinjaX can be cached efficiently. I haven't have to chase the last tens of milliseconds of each request, so I don't know. But having the page rendered and displayed right away (without the standard flurry of AJAX calls from React/Vue/Angular) feels fast to users.
With this setup in place, I can develop in a few days a full app that feels reactive and "live" to the user. As it uses boring technologies, it is easy, proven, a lot of resources exist, LLM handle everything very well, the code is clear, and more importantly, users are delighted.
Sure, you won't build the next Figma, but the DjangoXX stack covers 80% of possible SaaS features. Isn't that enough?