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.

Create a free account to save this page, take notes and track your progress. Sign in

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 (or HX-Location for 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"}

The code

Login: errors in place, HX-Redirect on successTry it yourself
<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-select as a stopgap).

Key points

  • Partials (or template fragments) keep page and fragment markup identical.
  • Choose the template by HX-Request; send Vary.
  • 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.

Was this page helpful?