HTMX and Go keep interactive UIs honest
Server-rendered fragments, almost no client JS, and response patterns that actually hold up.
The SPA tax shows up in places that have nothing to do with user experience: dual codebases, hydration edge cases, API contracts for every button, and a frontend build that needs its own release process. For a lot of Go services, that overhead is pure waste. Pairing HTMX with Go's server-side HTML is a practical way out. You keep rendering where the data already lives, ship a tiny dependency-free script, and still get list filters, form updates, and partial swaps that feel app-like.
This is not nostalgia. It is a deliberate bet that most interactive screens in developer tools, admin UIs, and form-heavy apps are better owned by the server. The stack is production-ready for that class of work. The hard part is not HTMX itself. It is response discipline: knowing when to return a fragment, when to return a full page, and how to handle redirects and errors without breaking browser history.
Why this pairing works
Go already ships net/http and html/template. HTMX sits on top of plain HTML attributes (hx-get, hx-post, hx-swap, hx-target) and turns clicks and form submits into AJAX requests that swap DOM regions. You do not invent a second data model for the browser. The same handlers that once returned full pages can return just the row, card, or table the client asked for.
Size matters here. HTMX is roughly 14KB minified and gzipped and has no framework runtime. Compare that to React DOM alone at around 42KB before any app code or meta-framework payload. For teams whose frontend work is mostly "update this panel after a form post," the smaller surface is a real maintenance win. You also avoid the usual split-brain problem where business rules live in Go and presentation state drifts in TypeScript.
HTML-over-the-wire is not unique to HTMX. The same template patterns transfer cleanly to tools like Unpoly or Hotwire if those fit your shop better. The Go side stays almost identical: compose templates, detect the request type, and choose full or partial output.
The response shape problem
Same route, two clients. A normal navigation wants a full document. An HTMX request wants a fragment. If you always render the base layout, you get nested shells and broken swaps. If you always return fragments, deep links and history restore fall apart.
The reliable pattern is request detection plus template composition:
- Keep a base layout with shared chrome (nav, scripts, CSS).
- Put page-specific blocks in named templates (title, main content).
- Put reusable chunks in partials (rows, forms, alerts).
- On each handler, check whether the request is an HTMX call (the
HX-Requestheader, or a small helper). History restore requests usually want a full page even when HTMX is involved.
Libraries such as go-htmx expose this as IsHxRequest, IsHxBoosted, IsHxHistoryRestoreRequest, and a combined RenderPartial check. You can do the same with a few lines of stdlib code. The point is the branch, not the package.
Redirects need the same care. A classic 302 confuses HTMX mid-swap. Prefer the HX-Redirect response header so the client navigates after the request finishes. Errors should either return a partial error UI targeted at a known region or use headers like HX-Retarget so the failure lands somewhere visible instead of silently replacing the wrong node.
What you actually adopt on day one
Start with static assets under your control. Download HTMX (and your CSS) into the binary or a static directory and serve them yourself. CDN convenience is fine for demos; production prefers a pinned file and defer on the script tag so parsing is not blocked while the DOM is still building.
A layout that works for most apps looks like this in spirit:
{{define "base"}}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{template "page:title" .}}</title>
<link rel="stylesheet" href="/static/css/app.css">
<script defer src="/static/js/htmx.min.js"></script>
</head>
<body>
<main>{{template "page:content" .}}</main>
</body>
</html>
{{end}}
Pages define page:title and page:content. Partials define named chunks you can execute alone. In the handler:
if isHTMX(r) && !isHistoryRestore(r) {
// execute only the partial
return tmpl.ExecuteTemplate(w, "users-table", data)
}
// full page: base + page content
return tmpl.ExecuteTemplate(w, "base", data)
Concrete UI patterns that map well:
- Filter a table with
hx-geton input change andhx-targeton the tbody. - Delete a row with
hx-deleteandhx-swap="outerHTML"so the row vanishes. - Create items with a form that posts and appends (
hx-swap="beforeend") or replaces a list region. - Use
hx-boostcarefully on links if you want progressive enhancement without rewriting every anchor.
If you want compile-time checks on templates, Templ is a solid alternative to html/template. You write .templ components, generate Go, and still return strings or stream HTML from the same handlers. Resource-oriented libraries (for example babyapi's HTMLer interface) can also switch between JSON and HTML based on Accept, which pairs cleanly with HTMX's preference for HTML fragments.
Trade-offs you should not paper over
HTMX does not give you a client-side state machine. Offline-first apps, heavy canvas work, rich drag-and-drop editors, and highly interactive dashboards with local-only state still want a real frontend framework. Do not force HTMX into those shapes.
You also take on more server traffic for fine-grained interactions. That is usually fine on Go, especially if partials are cheap and you cache where it counts, but chatty UIs need the same performance attention any other API would. Test history navigation and back-button behavior early; partial-only handlers that ignore history restore are a common footgun.
Team skills shift too. Frontend specialists who live in component trees may feel constrained. Backend-heavy teams move faster because a single PR owns the feature end to end. That is a feature for many product orgs, not a bug.
The honest cut
For CRUD, filters, multi-step forms, and internal tools, Go plus HTMX is not a compromise stack. It is often the simpler production default. You ship less JavaScript, keep one rendering path, and avoid inventing APIs for every UI event. Use a thin helper for HTMX headers if you like, or stay on the stdlib. Reach for Templ when stringly-typed templates start to hurt.
If your product is mostly server-owned state and progressive enhancement is enough, start here and stay until a real client-side requirement forces a heavier runtime. Most apps never hit that wall.
Sources & further reading
- How I use HTMX with Go — alexedwards.net
- GitHub - donseba/go-htmx: Seamless HTMX integration in golang applications · GitHub — github.com
- How To Build a Web Application with HTMX and Go - DEV Community — dev.to
- Tailbits | Setting up HTMX and Templ for Go — tailbits.com
- Go HTMX Integration: Build Modern Web Apps Without JavaScript — reintech.io
Rachel has been embedded in the developer tooling ecosystem for nearly eight years, covering everything from IDE wars and package-manager drama to the quiet rise of AI-assisted coding. She has a soft spot for open-source maintainers and an unhealthy number of terminal emulators installed on a single laptop.
Discussion 0
No comments yet
Be the first to weigh in.