Carson Gross, Adam Stepinski and Deniz Akşimşek have recently published their work, Hypermedia Systems and have generously made its entire content available online without any fee. Over the past two years or so, I’ve found myself reaching for HTMX more often when I build sites. It’s been a really nice way to work, but I’ve definitely learned it ad hoc.

Since there’s now this opportunity to directly see how its author thinks about using it, it seems worth taking a bit of time to work through the concepts and exercises in the book using django and htmx, which are easily my current favorite way to build for the web. I’m going to do this in public because it’s more fun to discuss it that way.

I plan to work through all of the web-focused chapters. I may leave the mobile-focused ones for another time. This post will detail my initial project configuration, will include a link to a git repository where anyone who’s interested can follow along, and will be updated with links to future posts as I write them.


Project Setup

Chapter 3 begins:

To start our journey into Hypermedia-Driven Applications, we are going to create a simple contact management web application called We will start with a basic, “Web 1.0-style” Multi-Page Application (MPA), in the grand CRUD (Create, Read, Update, Delete) tradition. It will not be the best contact management application in the world, but it will be simple and it will do its job.

Since I know I want to use django and htmx for this, I’ll start with just a little more boilerplate than the authors do in order to keep my project a bit neater. Everything here uses python 3.11.6 and Django 4.2.6, though I don’t expect it to be highly dependent on those specific versions. I’m working on a Mac, and nearly everything I do here should be identical on a Linux system. Some steps will look a little different on a Windows system, but I don’t have one handy to test them on, so I won’t include them here. I use pyenv to manage multiple versions of python on my system because I need different ones for different projects. If you don’t need this, just install using the package from for your system. Pyenv is just doing that for me, plus making them easy to switch.

For projects that I want to maintain long-term, I like to use poetry or pip-tools to manage the dependencies. Since this is just a quick exercise, I’m skipping those here. I am still using black, pre-commit, and djhtml, mostly because not using those makes my eye twitch. I use gibo to grab a gitignore boilerplate quickly, but you can also just go grab one from the web if that’s easier on your system.

mkdir contacts-hypermedia-systems
cd contacts-hypermedia-systems
pyenv local 3.11.6
python3 -mvenv venv
source venv/bin/activate
pip install black djhtml pre-commit django django_extensions
# If you use another OS or don't use JetBrains tools, skip macOS and JetBrains
gibo dump macOS JetBrains python >.gitignore

With the basics in place, I get my source repository set up:

git init .
git branch -m main
# my default pre-commit config snippet:
cat <<EOF >>.pre-commit-config.yaml
  - repo:
    rev: main
      - id: black
        language_version: python3.11
  - repo:
    rev: main
      - id: djhtml
# use this to fill in the versions
pre-commit autoupdate
# and this to set up the hooks themselves that will prevent me from committing messy python or messy django templates
pre-commit install

Now I start a django project and a contacts app. I like the convention of calling my project module “config” because that matches my mental model for what it does. That’s not in anyway load bearing here.

django-admin startproject config .
python3 ./ startapp contacts
mkdir templates

With that, I add "contacts" and "django_extensions" to INSTALLED_APPS in config/


and because I think it’s easier and clearer for a small project like this, I add a root level templates directory to the TEMPLATES configuration:

        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [BASE_DIR / "templates"],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [

Finally, I create a new view in contacts/ for an index page:

from django.http import HttpResponse
from django.shortcuts import render

def index(request):
    return HttpResponse("Hello, world!")

I create a contacts/ file to contain routes for the contacts application and add my view there:

from django.urls import path

from . import views

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

and I modify config/ to reference the contacts application’s url routes:

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

urlpatterns = [
    path("", include("contacts.urls")),

After that, I run the development server and visit with a browser to make sure everything is in place:

python runserver

Screenshot of Hello, World! in Firefox

Since my boilerplate is set, I’m ready to commit my work and push.

You can see my repository in progress at

Read on to see how I built out the Web 1.0 version of