Skip to main content
Technology & EngineeringHtmx271 lines

Htmx Backend Patterns

Server-side patterns for HTMX including partial templates, response headers, and middleware

Quick Summary21 lines
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 lines
Paste into your CLAUDE.md or agent config

Server-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:

HeaderEffect
HX-RedirectClient-side redirect to the given URL
HX-RefreshFull page refresh (true)
HX-RetargetOverride the target element on the client
HX-ReswapOverride the swap strategy on the client
HX-TriggerTrigger client-side events after the response settles
HX-Push-UrlPush a new URL into the browser history
HX-Replace-UrlReplace 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-Redirect response 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-Request on cached responses. When the same URL returns different content based on the HX-Request header, CDNs and browser caches may serve a cached fragment to a full-page request or vice versa. Always include the Vary header to key caches correctly.

  • Scattering HX-Request detection 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-Request in 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-Trigger response 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-Request header (use Vary: HX-Request in 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-Request on cached responses. CDNs and browser caches may serve a cached fragment to a full-page request (or vice versa). Always include Vary: HX-Request when 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-Redirect response header instead for client-side navigation after a POST.
  • 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.
  • 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-Request is present.

Install this skill directly: skilldb add htmx-skills

Get CLI access →