How I Start: Django, Tailwind, HTMX (part 1)

I wanted to try out some "modern" front-end development for a while, and I did. FastAPI is great. So is Svelte. But even though I found creating APIs that way to be a breeze, building a whole site that way felt much slower and heavier than using traditional server rendering. So I decided to give django a fair shake for a while, and I’m glad I did. Here’s how I currently like to get a site started with django, using Tailwind CSS for responsive layouts and styling, and mixing in alpine.js and HTMX to make the site feel reactive. I’m glad I gave the backend/frontend split an honest try, but this fits the way I think about sites better.

This post walks all the way through building a small site using this stack. My notes on how I like to get all these little things done had gotten spread in several places, and since I’m setting up a new development system, I decided to put everything together in a small project to try it out.

The site I’m building is a reading journal, similar to something a school kid might maintain to track required summer reading.

The finished project is available here.

Setup

I’m using pyenv to manage python installations on my system, and poetry to manage dependencies. I won’t focus much on these, but this post does a decent job explaining how I’m using them. If you don’t want to use these, just create a virtual environment and activate it, then install these packages. Any command I describe using poetry run can be executed in an activated virtual environment without poetry.

Note that django-tailwind requires npm to be installed on your path. I used nvm for that.

Starting Out

I want a clean space with the current most-recent python I’m willing to use, which is 3.10.4 as of today.

mkdir reading_journal
cd reading_journal
pyenv local 3.10.4
poetry init
git init

When poetry asks to specify dependencies and development dependencies interactively, I decline because I prefer to just add them on the command line.

I recently learned about gibo. It saves me a few steps in setting up my .gitignore file:

gibo dump Python JetBrains Emacs macOS VisualStudioCode VirtualEnv >>.gitignore

The only thing that’s really critical is that .env be ignored, since that’s where I’ll put any secrets.

poetry add -D black djhtml pre-commit
poetry add django django-tailwind django-htmx django-extensions django-environ humanfriendly django-crispy-forms crispy-tailwind

Useful detour: pre-commit hooks

Once dependencies are set up, I configure pre-commit to automatically run black and djhtml whenever I commit my project so that things stay formatted nicely. Nothing else in this project will break if you skip this, but I’ve found I really prefer having it from the beginning.

Here’s my starting .pre-commit-config.yaml

repos:
- repo: https://github.com/rtts/djhtml
  rev: main
  hooks:
  - id: djhtml
    files: .*/templates/.*\.html$
- repo: https://github.com/psf/black
  rev: main
  hooks:
  - id: black

I put that in place and run two commands to get it set up:

poetry run pre-commit install
poetry run pre-commit autoupdate

which will print two warnings about fixing the hook versions, and update .pre-commit-config.yaml:

repos:
- repo: https://github.com/rtts/djhtml
  rev: v1.5.0
  hooks:
  - id: djhtml
    files: .*/templates/.*\.html$
- repo: https://github.com/psf/black
  rev: 22.3.0
  hooks:
  - id: black

Project Creation

I like to call my project “config” because that is how I think of the things that are found in the module. Use whatever makes sense to you

poetry run django-admin startproject config .

Once I’ve run startproject I tend to fire up PyCharm and do everything else from the integrated terminal there.

Before creating my “real” app, I need to add “tailwind” to settings.py so that the tailwind management commands will work.

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "tailwind",
]

Then I use poetry run python manage.py tailwind init to create an app called theme which will contain tailwind configuration. When that completes, I add "theme" to INSTALLED_APPS, and add the required settings:

# as described in https://django-tailwind.readthedocs.io/en/3.3.0/installation.html
TAILWIND_APP_NAME = "theme"
INTERNAL_IPS = [
    "127.0.0.1",
]

then run poetry run manage.py tailwind install to finish setting up tailwind.

Now that tailwind is ready, I create my primary application:

poetry run python manage.py startapp reading_journal

and add it to INSTALLED_APPS.

Rendering and Running

With the main app in place, it’s time to set up a template, a view, a route, and configure pycharm to run things nicely.

reading_journal/templates/reading_journal/base.html:

{% load static tailwind_tags %}
<!DOCTYPE html>
<html lang="en">
<head>
   <title>{% block title %}Reading Journal{% endblock title %}</title>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <meta http-equiv="X-UA-Compatible" content="ie=edge">
   {% tailwind_css %}
</head>

<body>
<div class="container mx-auto">
   {% block content %}
       <div class="bg-blue-100">
           <section class="flex items-center justify-center h-screen">
               <h1 class="text-5xl">Django + Tailwind = ❤️</h1>
           </section>
       </div>
   {% endblock content %}
</div>
</body>
</html>

reading_journal/templates/reading_journal/index.html:

{% extends 'reading_journal/base.html' %}

reading_journal/views.py:

from django.http import HttpRequest
from django.shortcuts import render


def index(request: HttpRequest):
   return render(request, "reading_journal/index.html", {})

reading_journal/urls.py:

from django.urls import path, include

from . import views

urlpatterns = [
    path("", views.index, name="index"),
]

config/urls.py:

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", include("reading_journal.urls")),
]

Then I test it out by running poetry run python manage.py tailwind start along with poetry run python manage.py runserver. A blue box declaring my love for tailwind means everything is in the right place. If I see that, I proceed to modify reading_journal/templates/reading_journal/index.html:

{% extends 'reading_journal/base.html' %}
{% block content %}
    <div class="bg-red-500">
    <div class="w-screen h-screen">
        <h1 class="text-8xl">Reading Journal</h1>
    </div>
    </div>
{% endblock content %}

and confirm that visiting / gives me black text in a red box.

With that running, I set up PyCharm to start and stop things nicely. First, set up a run configuration for the tailwind watcher:

tailwind watcher

Then set up a compound task that runs both the django server and the tailwind watcher:

compound task

With the basics in place, I like to add django_browser_reload so that I no longer need to refresh the browser to see changes after I edit my django templates.

poetry add django-browser-reload

config/settings.py:

INSTALLED_APPS = [
  ...,
  'django_browser_reload'
]
MIDDLEWARE = [
  # ...
  "django_browser_reload.middleware.BrowserReloadMiddleware",
  # ...
]

config/urls.py:

urlpatterns = [
    ...,
    path("__reload__/", include("django_browser_reload.urls")),
]

To test, I start my compound task and point a browser at http://localhost:8000/ to confirm there’s a red square with black text. Then I change the background color on my index.html template to bg-green-500 and switch back to the browser. The background color should change in the browser without any need to force a refresh.

My desire to write down everything I do from the very beginning has made this longer than I expected. More to come in part 2.


I’m trying on Kev Quirk’s “100 Days To Offload” idea. You can see details and join yourself by visiting 100daystooffload.com.

This is day #2.