HTML Over the Wire
Choosing HTMX
Lesson 5 of 5
What you'll learn
- Judge honestly where hypermedia wins and where a React SPA is the better call
- Recognize the signals: who owns the state, and how much lives in the client
- Ship the whole app as one static binary with htmx vendored and embedded
htmx has a loud fan club, so let's be precise instead of loyal. The deciding question is: where does the interesting state live?
Hypermedia wins when state lives on the server. CRUD apps, admin panels, internal tools, dashboards, anything form-shaped: every interaction is naturally "change something, show the new truth." That's one request and one fragment. What you don't build is the entire middle layer a SPA requires — no JSON API, no fetch helpers, no client-side state manager, no loading-state choreography, no frontend build pipeline. One language, one repo, one process. For this class of app, that's not a compromise; it's strictly less machinery for the same result.
A SPA wins when state lives in the client. Some interfaces are client state: a drag-and-drop board editor, a canvas tool, a spreadsheet. Realtime collaboration wants a client model that patches on every websocket event. Offline-first needs a local store that syncs later. And heavy optimistic UI — pretending the server already answered — requires a client that knows what the answer should be. Modeling any of these as request-per-interaction fights the grain; round trips land where client memory belongs. That's the other full-stack Go shape — a Go API with a React frontend — and it's a different course.
There's a legitimate middle ground. Hypermedia doesn't mean zero JavaScript — a sprinkle of vanilla JS or a small library for one lively widget (a date picker, a chart) inside an otherwise server-rendered page is normal and healthy. The page stays hypermedia; the widget is an island.
The signal to watch for is drift: if your htmx app grows enough custom JavaScript that the client is second-guessing the server's HTML — mirroring its state, re-rendering its fragments — you've built a SPA with extra steps. Switch shapes deliberately instead.
Two things htmx handles better than its reputation suggests: URLs and history (hx-push-url keeps the back button honest), and perceived speed — a fragment response is typically smaller and faster than a JSON round trip plus a client render.
Shipping it
Everything from the previous course applies unchanged — this whole app is one CGO_ENABLED=0 static binary. htmx itself is a single file (~14 kB gzipped), so vendor it and embed it next to your templates:
//go:embed static templates
var assets embed.FS
// mux.Handle("GET /static/", http.FileServerFS(assets))
Vendor htmx; don't hotlink a CDN
Download htmx.min.js into static/ and let //go:embed bake it in. Your app now has zero runtime dependencies on the outside world — no CDN outage, no supply-chain surprise from a moved script, and it works on an air-gapped network. Pin upgrades with a deliberate file replacement, on your schedule.
The challenge is the decision guide as runnable code: score two very different apps and see the recommendation — with reasons — fall out.
Run it. Each requirement scores for hypermedia or a SPA; decide() totals them per app and prints a recommendation with reasoning. Score your own current project by editing the needs lists.
Which app is the strongest case for Go + HTMX over a React SPA?
That's the hypermedia arc complete: attributes trigger requests, handlers answer with template fragments, validation round-trips inline, SQLite in WAL mode keeps up underneath — and the whole app ships as one static Go binary that owns its own frontend.
Saved on this device. Sign in to sync your progress everywhere.