htmx: Build Interactive Web Apps with HTML · Production Concerns
Organising the Server: Partials, Fragments and Headers
Structure templates so the same markup serves full pages and htmx fragments, and use response headers for redirects and events.
What you'll build
The server side of the contacts app: one template for the page and a partial for the rows, a handler that picks the
right one, and a login handler that redirects with HX-Redirect.
The idea
- Partials: put each fragment htmx requests (rows, a form, a card) in its own template and include it from the full page. Same markup in both places, written once.
- Template fragments: some engines render a named block from a page template (Django's
django-template-partials,jinja2-fragments, Rails' and Laravel's fragment helpers), so the fragment can stay inside the page template. - Response headers let the server steer:
HX-Redirect(full redirect after login),HX-Trigger(events),HX-Retarget/HX-Reswap(put an error somewhere else),HX-Push-Url,HX-Refresh. - Don't answer htmx with a 302 redirect: XMLHttpRequest follows it silently and htmx swaps the redirected page into
the target. Use
HX-Redirect(orHX-Locationfor an htmx-style navigation).
@app.get("/contacts")
def contacts():
rows = Contact.search(request.args.get("q", ""))
htmx = request.headers.get("HX-Request") and not request.headers.get("HX-History-Restore-Request")
html = render_template("contacts/_rows.html" if htmx else "contacts/index.html", rows=rows)
return html, {"Vary": "HX-Request"}
@app.post("/login")
def login():
user = authenticate(request.form["email"], request.form["password"])
if not user:
return render_template("auth/_login_form.html", error="Wrong email or password."), 422
login_user(user)
return "", 200, {"HX-Redirect": "/dashboard"}
Route::post('/login', function (Request $request) {
if (! Auth::attempt($request->only('email', 'password'))) {
return response()->view('auth._login_form', ['error' => 'Wrong email or password.'], 422);
}
$request->session()->regenerate();
return response('', 200)->header('HX-Redirect', '/dashboard');
});
The code
<meta name="htmx-config" content='{"responseHandling": [{"code": "204", "swap": false}, {"code": "422", "swap": true},
{"code": "[23]..", "swap": true}, {"code": "[45]..", "swap": false, "error": true}]}'>
<form hx-post="/login" hx-swap="outerHTML">
<input name="email" value="[email protected]"> <input name="password" type="password" placeholder="try: secret">
<button>Log in</button>
</form>
<!-- The pretend server: on a real site this part is your backend (Python, PHP, Node, Go...) -->
<script>
server.post("/login", (req) => req.body.password === "secret"
// A real server sends HX-Redirect here; in the preview it would load a new page inside the frame, so it's described.
? { body: "<p>Logged in. A real server would answer with <code>HX-Redirect: /dashboard</code>.</p>" }
: { status: 422, body: `<form hx-post="/login" hx-swap="outerHTML">
<input name="email" value="${server.escape(req.body.email)}"> <input name="password" type="password">
<button>Log in</button><p role="alert" style="color:#b91c1c">Wrong email or password.</p></form>` });
</script>
Common mistakes
- Duplicating markup: one copy in the page template and another in the htmx handler. They drift apart.
- A 302 after an htmx POST: the dashboard ends up inside the login form.
- Putting business logic in templates "because htmx is HTML": keep handlers thin and logic in your domain code.
- Returning a full page to an htmx request that targets a small element: use a partial (or
hx-selectas a stopgap).
Key points
- Partials (or template fragments) keep page and fragment markup identical.
- Choose the template by
HX-Request; sendVary. - Use
HX-Redirect, not 302, to leave the page after an htmx request.
Further reading: htmx essay: template fragments · htmx response headers · Tutorial: The server side
Practice questions
Why use HX-Redirect instead of a 302 after a successful htmx login?
Answer
XMLHttpRequest follows 302s transparently, so htmx would swap the dashboard into the form. HX-Redirect makes the browser navigate.
What are template fragments and why do they help htmx apps?
Answer
Rendering a named part of a template on its own, so the fragment htmx requests and the full page share one source of markup.
How does a handler decide between a partial and the full page?
Answer
Check the HX-Request header (and HX-History-Restore-Request), render the partial for htmx and the full page otherwise, and send Vary: HX-Request.
Open in the app to mark lessons complete, take notes and track your progress.