Six months ago I was skeptical. HTMX sounded like a fun experiment — a way to build interactive UIs without writing a full React app. But I'd been burned by "simple" approaches before. Simple usually means "simple until it isn't," and then you're stuck with the wrong tool in the wrong place.
We built Partida on FastAPI and HTMX anyway. Six months in, shipping to real users. Here's what I actually think.
## The good
The thing nobody talks about enough: **you stop thinking in two layers**. With a React frontend and a REST API, you're constantly making decisions at the boundary — what does the API return? What does the client transform? Who owns that piece of state? With HTMX, the server returns HTML. The client swaps it in. That's it. The cognitive overhead drops considerably.
We also found debugging got easier. When something looks wrong on screen, the answer is almost always in the HTML the server returned — not buried in a chain of state updates. `hx-boost` and `hx-swap` are visible attributes in the DOM. The behavior is inspectable without a browser devtools plugin.
## The surprising
Forms are excellent with HTMX. Server-side validation errors, inline feedback, partial page updates — all clean and fast. Where it gets harder is anything with *shared state across unrelated components*. If a user action on one side of the page needs to update something on the other side that the server doesn't control, you're reaching for `hx-trigger` and custom events, and it starts to feel like patching a hole.
The real-time chat feature in Partida — which uses Server-Sent Events — required a thin layer of vanilla JS alongside HTMX. SSE itself is straightforward, but coordinating the incoming event stream with an HTMX-managed DOM took some careful thought. Not a dealbreaker, but worth knowing before you commit.
## The one pattern I'd change
We started by having every endpoint return a full HTML fragment. That works, but it creates tight coupling between route and UI. Halfway through, we moved to a pattern where templates have dedicated partial variants — `_case_row.html` alongside `case_row.html` — and the route decides which to render based on the request headers HTMX sets. Much cleaner to test, much easier to reason about when the page structure changes.
If you're starting fresh: build that convention in from the beginning. You'll thank yourself later.