A hands-on workshop project for Alamo Tech Collective (in collaboration with Alamo Python) that builds a to-do app with Django + HTMX — instant, partial-page updates and not a line of hand-written JavaScript.
Every action in this app — add a task, check it off, delete it, flip to the next
page — updates just the part of the page that changed, with no full-page reload.
The trick is HTMX: a small library you load from a CDN, a handful of hx-*
attributes in the templates, and Django views that return HTML fragments instead
of whole pages. That's the whole story, and this app is here to show it end to end.
- Docker Desktop installed and running
- Git (for cloning the repository)
- That's it! No Python, Django, or other dependencies needed locally.
git clone https://github.com/Alamo-Tech-Collective/django-htmx-todo.git
cd django-htmx-tododocker compose upWait for the startup messages. You should see:
Starting development server at http://0.0.0.0:8000/
Visit: http://localhost:8000
You should see the to-do app running!
- Add some tasks
- Check/uncheck tasks (just that row updates — no reload)
- Delete tasks (the row disappears — no reload)
- Add 6+ tasks to see pagination (paging swaps just the list, and the URL still
updates so you can bookmark
?page=2)
Key observation: Nothing flashes or reloads. Open your browser's Network tab and watch — each action is a tiny request that returns a snippet of HTML, and HTMX drops it into the page.
Press Ctrl+C in the terminal, or run:
docker compose downThe whole HTMX integration is three moves — worth tracing once you've got the app running:
- Load HTMX. One CDN
<script>tag intasks/templates/tasks/base.html. That same file setshx-headerson<body>so Django's CSRF token rides along on every HTMX request. - Mark up the controls. In the templates, the add form, the checkboxes, the
delete buttons, and the pagination links carry
hx-post/hx-get,hx-target, andhx-swap— telling HTMX what to request and where to put the response. - Return fragments.
tasks/views.pyreturns a partial template (a single row, or the list region) instead of a full page. Comparetasks/templates/tasks/task_list.html(the full page) withtasks/templates/tasks/partials/(the pieces the views swap in).
Trace one action end to end — toggling a checkbox is a good one — and you've seen basically everything this app does.
django-htmx-todo/
├── README.md # This file
├── CLAUDE.md # Project context and guidelines
├── docker-compose.yml # Docker orchestration
├── Dockerfile # Container definition
├── requirements.txt # Python dependencies
├── manage.py # Django management script
├── todo_project/ # Django project settings
│ ├── settings.py
│ └── urls.py
└── tasks/ # To-do app
├── models.py # Task model
├── views.py # View logic
├── forms.py # Django forms
├── urls.py # URL routing
├── admin.py # Django admin
├── templates/ # HTML templates
│ └── tasks/
│ ├── base.html # loads HTMX, sets the CSRF header
│ ├── task_list.html # full page: add form + the list partial
│ └── partials/ # the fragments the views swap in
│ ├── task_row.html # one task <li>
│ └── task_list.html # the list region + pagination
└── static/ # CSS
└── css/
└── style.css
- ✅ Create tasks (the list region re-renders, no full reload)
- ✅ View tasks (with pagination — 5 per page, swapped via HTMX)
- ✅ Toggle task completion (just that row updates)
- ✅ Delete tasks (the row is removed from the DOM)
- ✅ Django admin interface
- Django views return HTML fragments, not just full pages
hx-post/hx-get+hx-target+hx-swapwire the UI to those views- CSRF handled once via
hx-headerson<body> - No hand-written JavaScript — HTMX is the only script on the page
This project is designed to teach:
- The HTMX model — request, target, swap; HTML over the wire
- The Django side — returning partials and reusing templates via
{% include %} - When to use HTMX vs full JavaScript frameworks
- Real-world patterns you can use in your projects
Plan for about 90–120 minutes, hands-on the whole way. Here's the shape of it:
Phase 1 — Run it & read it (~40–50 min). Everyone gets the app running with
docker compose up, uses it (notice: nothing reloads), then we walk the code —
models, views, forms, and the templates. We'll trace one action, like toggling a
task, from the hx-post in the template all the way to the view that returns the row.
Phase 2 — Build a feature (~30–45 min). We cover the HTMX fundamentals
(hx-post/hx-get, hx-target, hx-swap) and how a Django view returns a
fragment, then add a feature together and you take a crack at one yourself.
Phase 3 — Q&A and wrap-up (~15 min). When to reach for HTMX vs. React/Vue, scaling considerations, and where to go next.
Come with Docker Desktop installed and the repo cloned (see Setup above). The less time we spend on environment issues, the more we spend building.
Problem: Cannot connect to the Docker daemon
# Solution: Make sure Docker Desktop is running
# Then try again: docker compose upProblem: Port 8000 already in use
# Solution: Stop the other process using port 8000, or change the port in docker-compose.yml
# Find what's using port 8000:
lsof -i :8000
# Kill it or change docker-compose.yml ports to "8001:8000"Problem: Changes not appearing
# Solution: Rebuild the container
docker compose down
docker compose up --buildProblem: No tasks showing up
# Solution: Check if migrations ran
docker compose exec web python manage.py migrateProblem: CSS not loading
# Solution: Collect static files
docker compose exec web python manage.py collectstatic --noinput
# Or just refresh the page (volume mounting should auto-update)# Create a superuser for Django admin
docker compose exec web python manage.py createsuperuser
# Access Django admin at: http://localhost:8000/admin
# Create migrations (if you modify models)
docker compose exec web python manage.py makemigrations
# Apply migrations
docker compose exec web python manage.py migrate
# Django shell
docker compose exec web python manage.py shell
# Run the test suite
docker compose exec web python manage.py test# Follow logs in real-time
docker compose logs -f web
# View last 50 lines
docker compose logs --tail=50 web- Backend: Django 5.2 (LTS)
- Database: SQLite (simple, no external dependencies)
- Frontend: Django templates + HTMX 2.x (loaded from CDN)
- Styling: Minimal custom CSS
- Container: Docker + Docker Compose
This is an educational project for Alamo Tech Collective workshops. Contributions are welcome!
MIT License - feel free to use this for your own learning and projects!
- Workshop attendees: Ask during the workshop or in the Slack channel
- Everyone else: Open an issue on GitHub
- Visit us: Alamo Tech Collective
Built with ❤️ by Alamo Tech Collective & Alamo Python community