HTMX
02 / 02

Boosted Navigation, OOB Swaps & Backend Patterns

HTMX: Boosted Navigation, OOB Swaps & Backend Patterns

hx-boost: Progressive Enhancement

<!-- hx-boost converts normal <a> links and <form> submissions inside
     this container into AJAX requests that update just the body,
     while keeping standard href/action attributes -- degrades
     gracefully to normal navigation if JS fails to load -->
<div hx-boost="true">
  <a href="/about">About</a>
  <a href="/contact">Contact</a>
</div>

<!-- hx-push-url keeps the URL bar and browser history in sync with
     an AJAX-driven navigation -- back/forward and refresh work
     correctly, as if a real page load happened -->
<a hx-get="/products/42" hx-push-url="true" hx-target="#content">
  View Product
</a>

Out-of-Band Swaps

<!-- Server response for "Add to cart": updates the main target
     (the button becomes a confirmation) AND, in the same response,
     updates the cart badge count ELSEWHERE on the page via a
     matching id + hx-swap-oob -->
<button hx-post="/cart/add" hx-target="this" hx-swap="outerHTML">
  Add to cart
</button>

<!-- Server response HTML: -->
<!--
<button>Added ✓</button>
<span id="cart-count" hx-swap-oob="true">3</span>
-->
<!-- HTMX swaps the button normally, AND finds the existing
     #cart-count on the page and swaps it too -- one round-trip,
     two page locations updated -->

Server-Side: Detecting HTMX Requests

// Express example -- one route serves BOTH a full page (direct
// navigation) and just the fragment (HTMX-triggered update) by
// checking the HX-Request header HTMX adds automatically
app.get('/messages', async (req, res) => {
  const messages = await getMessages();

  if (req.headers['hx-request']) {
    // Just the fragment -- reuses the same partial template
    res.render('partials/message-list', { messages });
  } else {
    // Full page, direct browser navigation
    res.render('pages/messages', { messages });
  }
});

// Validation errors: re-render the same form partial with inline
// errors instead of a separate JSON error-handling path
app.post('/messages', async (req, res) => {
  const errors = validate(req.body);
  if (errors.length) {
    return res.status(422).render('partials/message-form', { errors, values: req.body });
  }
  const message = await createMessage(req.body);
  res.render('partials/message-item', { message });
});

Extensions & Hypermedia-Driven Apps

  • HTMX's core stays small and focused on request/swap behavior -- SSE, WebSockets, and other capabilities ship as opt-in extensions.

  • "Hypermedia-Driven Applications": the server owns both application state AND its HTML representation -- the client just requests and swaps fragments, avoiding duplicated client/server rendering logic.

  • Because the contract is just "return HTML," any server-side templating stack (Django, Rails, Laravel, Express) can serve HTMX requests -- no dedicated JSON API layer required.

  • Best fit: CRUD-heavy, mostly-server-driven apps. A framework with richer client-side state (React, etc.) may still fit better for deeply stateful or offline-capable client interactions.

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free