Htmx Backend Patterns
Server-side patterns for HTMX including partial templates, response headers, and middleware
You are an expert in building server-side architectures that support HTMX-driven frontends, including partial template rendering, response header management, and middleware patterns across multiple backend frameworks.
## Key Points
- **Full page request:** Renders the content block wrapped in the base layout (head, nav, footer).
- **HTMX request:** Renders only the content block, skipping the layout wrapper.
- **Use a naming convention for partial templates.** Prefix partials with an underscore (`_list.html`, `_form.html`) to distinguish them from full-page templates.
- **Centralize HTMX detection in middleware.** Rather than checking `HX-Request` in every view, add a middleware that sets a boolean on the request object.
- **Returning JSON by default.** API-first backends often return JSON. HTMX expects HTML. Either add content negotiation or create dedicated HTMX endpoints that return rendered templates.
- **Ignoring the `HX-Target` header.** The server knows which element will receive the content. Use this to return different levels of detail or to select the appropriate partial template.
## Quick Example
```html
<!-- partials/_cart_badge.html -->
<span id="cart-badge" hx-swap-oob="true" class="badge">
{{ count }}
</span>
```skilldb get htmx-skills/Htmx Backend PatternsFull skill: 271 linesServer-Side Patterns — HTMX
You are an expert in building server-side architectures that support HTMX-driven frontends, including partial template rendering, response header management, and middleware patterns across multiple backend frameworks.
Overview
HTMX's power depends on a well-structured server. The server must detect HTMX requests, return HTML fragments (not full pages), manage response headers for client-side behavior, and organize templates so that both full-page renders and partial fragment renders share the same source of truth. This skill covers backend patterns that apply across frameworks (Django, Flask, Rails, Go, Express, Laravel, Spring Boot, and others).
Core Concepts
Detecting HTMX Requests
Every HTMX request includes the header HX-Request: true. The server checks this header to decide whether to return a full HTML page or just a fragment.
Partial vs. Full Templates
A common pattern is template inheritance with a switchable base:
- Full page request: Renders the content block wrapped in the base layout (head, nav, footer).
- HTMX request: Renders only the content block, skipping the layout wrapper.
HTMX Response Headers
The server can influence HTMX behavior by setting response headers:
| Header | Effect |
|---|---|
HX-Redirect | Client-side redirect to the given URL |
HX-Refresh | Full page refresh (true) |
HX-Retarget | Override the target element on the client |
HX-Reswap | Override the swap strategy on the client |
HX-Trigger | Trigger client-side events after the response settles |
HX-Push-Url | Push a new URL into the browser history |
HX-Replace-Url | Replace the current URL in the browser history |
Out-of-Band (OOB) Swaps
The response can include additional HTML elements with hx-swap-oob="true" that update other parts of the page outside the primary target. This lets a single response update multiple regions (e.g., update a table row AND a notification badge).
Implementation Patterns
Django — Partial Template Pattern
# views.py
from django.shortcuts import render
def contact_list(request):
contacts = Contact.objects.all()
template = "contacts/_list.html" if request.headers.get("HX-Request") else "contacts/list.html"
return render(request, template, {"contacts": contacts})
<!-- contacts/list.html (full page) -->
{% extends "base.html" %}
{% block content %}
<h1>Contacts</h1>
<div id="contact-list">
{% include "contacts/_list.html" %}
</div>
{% endblock %}
<!-- contacts/_list.html (fragment) -->
{% for contact in contacts %}
<div class="contact-card">
<h3>{{ contact.name }}</h3>
<p>{{ contact.email }}</p>
</div>
{% endfor %}
Django — Middleware for HTMX Detection
# middleware.py
class HtmxMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
request.htmx = request.headers.get("HX-Request") == "true"
request.htmx_target = request.headers.get("HX-Target", "")
request.htmx_trigger = request.headers.get("HX-Trigger", "")
return self.get_response(request)
Express.js — Fragment Rendering
// middleware/htmx.js
function htmxDetect(req, res, next) {
req.htmx = req.headers["hx-request"] === "true";
req.htmxTarget = req.headers["hx-target"] || "";
// Helper to render full or partial
res.renderFragment = function(fullTemplate, partialTemplate, data) {
const template = req.htmx ? partialTemplate : fullTemplate;
res.render(template, data);
};
next();
}
// routes/contacts.js
app.get("/contacts", htmxDetect, async (req, res) => {
const contacts = await Contact.findAll();
res.renderFragment(
"contacts/list", // full page
"contacts/_list", // fragment
{ contacts }
);
});
Go — Template Composition
// handler.go
func ContactList(w http.ResponseWriter, r *http.Request) {
contacts := getContacts()
data := map[string]interface{}{"Contacts": contacts}
if r.Header.Get("HX-Request") == "true" {
tmpl := template.Must(template.ParseFiles("templates/contacts/_list.html"))
tmpl.Execute(w, data)
return
}
tmpl := template.Must(template.ParseFiles(
"templates/base.html",
"templates/contacts/list.html",
"templates/contacts/_list.html",
))
tmpl.ExecuteTemplate(w, "base", data)
}
Using HX-Trigger for Toast Notifications
# Django view
from django.http import HttpResponse
import json
def delete_contact(request, pk):
contact = Contact.objects.get(pk=pk)
contact.delete()
response = HttpResponse(status=200)
response["HX-Trigger"] = json.dumps({
"showToast": {"message": f"{contact.name} deleted", "level": "success"}
})
return response
Client-side listener:
<script>
document.body.addEventListener("showToast", function(evt) {
const { message, level } = evt.detail;
// Show toast notification using your preferred method
showToast(message, level);
});
</script>
Out-of-Band Swap — Update Multiple Regions
# Django view for adding an item to cart
def add_to_cart(request):
item = add_item_to_cart(request)
cart = get_cart(request)
html = render_to_string("cart/_item_row.html", {"item": item})
# OOB: also update the cart badge in the header
html += render_to_string("partials/_cart_badge.html", {"count": cart.item_count})
return HttpResponse(html)
<!-- partials/_cart_badge.html -->
<span id="cart-badge" hx-swap-oob="true" class="badge">
{{ count }}
</span>
Rails — Turbo-Style Partials
# contacts_controller.rb
class ContactsController < ApplicationController
def index
@contacts = Contact.all
if request.headers["HX-Request"]
render partial: "contacts/list", locals: { contacts: @contacts }
else
render :index
end
end
def destroy
@contact = Contact.find(params[:id])
@contact.destroy
head :ok, "HX-Trigger" => "contactDeleted"
end
end
Response Status Codes
# Django view with proper status codes
def create_contact(request):
form = ContactForm(request.POST)
if form.is_valid():
contact = form.save()
response = render(request, "contacts/_row.html", {"contact": contact})
response.status_code = 201
response["HX-Trigger"] = "contactCreated"
return response
else:
response = render(request, "contacts/_form.html", {"form": form})
response.status_code = 422
return response
Core Philosophy
HTMX moves the complexity of dynamic user interfaces from the client back to the server. The server is no longer just an API that returns JSON; it is a rendering engine that returns ready-to-display HTML fragments. This shift means your backend templates, view functions, and response headers become the primary tools for building interactive experiences, not JavaScript frameworks running in the browser.
The dual-response pattern, where the same URL returns a full page for normal requests and a fragment for HTMX requests, is the foundation of this architecture. It ensures every URL is bookmarkable, shareable, and functional without JavaScript, while still providing a smooth AJAX experience when HTMX is present. The HX-Request header is the simple mechanism that makes this possible, and checking it should be centralized in middleware rather than scattered across individual views.
Response headers like HX-Trigger, HX-Redirect, and HX-Retarget give the server fine-grained control over client behavior without embedding logic in JavaScript. The server can trigger toast notifications, redirect after a form submission, or change the swap target dynamically. This keeps the client side declarative and predictable while the server retains full authority over application flow.
Anti-Patterns
-
Returning JSON from endpoints consumed by HTMX. HTMX expects HTML, not JSON. Sending JSON forces you to write client-side templating code, defeating the purpose of the hypermedia approach. Create dedicated HTML-returning endpoints or add content negotiation to existing ones.
-
Using 301/302 redirects after HTMX POST requests. The browser transparently follows 301/302 redirects before HTMX sees the response, causing the redirected page's HTML to be swapped into the target. Use the
HX-Redirectresponse header for client-side navigation after mutations. -
Duplicating template markup between full-page and fragment renders. If the fragment served to HTMX and the fragment included in the full-page template diverge, the two rendering paths produce different HTML. Use template includes or partials to ensure a single source of truth.
-
Not setting
Vary: HX-Requeston cached responses. When the same URL returns different content based on theHX-Requestheader, CDNs and browser caches may serve a cached fragment to a full-page request or vice versa. Always include theVaryheader to key caches correctly. -
Scattering
HX-Requestdetection across every view function. Checking the header in every handler leads to inconsistency and forgotten checks. Centralize HTMX detection in middleware that sets a boolean on the request object, and use a helper function for template selection.
Best Practices
- Use a naming convention for partial templates. Prefix partials with an underscore (
_list.html,_form.html) to distinguish them from full-page templates. - Centralize HTMX detection in middleware. Rather than checking
HX-Requestin every view, add a middleware that sets a boolean on the request object. - Return appropriate HTTP status codes. Use 200 for success, 201 for creation, 422 for validation errors, 204 for no-content responses. HTMX processes all status codes, but proper codes help with debugging and logging.
- Use
HX-Triggerresponse headers for side effects. Instead of coupling the response HTML to UI side effects (toasts, modals, counters), trigger named events and let the client handle presentation. - Cache HTMX fragment responses carefully. HTMX requests and full-page requests to the same URL return different content. Vary the cache key on the
HX-Requestheader (useVary: HX-Requestin the response). - Share template fragments between full and partial renders. Use template includes/partials so the fragment served to HTMX is the same markup used in the full page. This prevents drift between the two rendering paths.
Common Pitfalls
- Not setting
Vary: HX-Requeston cached responses. CDNs and browser caches may serve a cached fragment to a full-page request (or vice versa). Always includeVary: HX-Requestwhen the same URL returns different content based on the header. - Returning JSON by default. API-first backends often return JSON. HTMX expects HTML. Either add content negotiation or create dedicated HTMX endpoints that return rendered templates.
- Redirects with 301/302 status codes. The browser transparently follows 301/302 redirects before HTMX sees the response. HTMX then swaps the redirected page into the target. Use the
HX-Redirectresponse header instead for client-side navigation after a POST. - Ignoring the
HX-Targetheader. The server knows which element will receive the content. Use this to return different levels of detail or to select the appropriate partial template. - Not handling the no-JavaScript fallback. If HTMX fails to load, forms and links should still work as traditional navigation. Design your server to return full pages by default and fragments only when
HX-Requestis present.
Install this skill directly: skilldb add htmx-skills
Related Skills
Htmx Active Search
Active search, typeahead, and autocomplete patterns using HTMX triggers and debouncing
Htmx Alpine Integration
Combining Alpine.js with HTMX for client-side state and interactivity alongside hypermedia-driven updates
Htmx Basics
Core HTMX attributes and fundamental patterns for hypermedia-driven web development
Htmx Forms
Form handling, validation, and submission patterns with HTMX
Htmx Infinite Scroll
Infinite scroll and lazy loading patterns using HTMX revealed and intersect triggers
Htmx Progressive Enhancement
Progressive enhancement strategies for building HTMX applications that work without JavaScript