htmx-best-practices
Mejores prácticas para construir UIs con HTMX: hipermedia como motor de estado, fragmentos de HTML desde el servidor, swaps, y mejora progresiva. Encaja con backends que renderizan HTML (FastAPI + Jinja, Astro endpoints, etc.).
Overview
HTMX permite interactividad rica sin SPA: el servidor devuelve fragmentos de HTML y HTMX los inserta en el DOM. El estado vive en el HTML (hipermedia), no en un store de JS.
Ideal para:
- Apps CRUD, dashboards internos, formularios dinámicos
- Reducir JavaScript y complejidad de build
- Backends que ya renderizan HTML (FastAPI/Jinja, Astro)
Principios
- El servidor devuelve HTML, no JSON. Los endpoints HTMX renderizan fragmentos.
- El HTML es la fuente de estado. Evitá duplicar estado en JS.
- Mejora progresiva. La página debe funcionar (o degradar con dignidad) sin JS.
Atributos core
<!-- Cargar un fragmento al hacer click y reemplazar el contenido -->
<button hx-get="/items?page=2"
hx-target="#list"
hx-swap="innerHTML">
Cargar más
</button>
<div id="list">...</div>
hx-get|post|put|patch|delete: verbo + URL.hx-target: dónde va la respuesta (selector CSS).hx-swap: cómo se inserta (innerHTML,outerHTML,beforeend,delete…).hx-trigger: qué evento dispara (click,submit,keyup changed delay:300ms,revealed,load).
Patrones útiles
Formulario con validación server-side
<form hx-post="/users" hx-target="#form-wrapper" hx-swap="outerHTML">
<input name="email" required>
<button type="submit">Crear</button>
</form>
El servidor devuelve el mismo formulario con errores, o el estado de éxito.
Búsqueda activa (active search)
<input type="search" name="q"
hx-get="/search" hx-target="#results"
hx-trigger="input changed delay:300ms">
Actualizar varias zonas — Out of Band swaps
<!-- respuesta del servidor -->
<div id="cart-count" hx-swap-oob="true">3</div>
<div id="main">...contenido principal...</div>
Integración con el backend
- Detectá peticiones HTMX con el header
HX-Request: truepara devolver el fragmento en vez de la página completa. - Usá headers de respuesta HTMX:
HX-Redirect,HX-Trigger(disparar eventos JS),HX-Retarget,HX-Reswap. - Devolvé el status HTTP correcto:
422para errores de validación re-renderiza el form;204cuando no hay contenido que insertar.
# FastAPI
@router.get("/items")
def items(request: Request):
template = "items_fragment.html" if request.headers.get("HX-Request") else "items_page.html"
return templates.TemplateResponse(template, {...})
Seguridad
- CSRF: incluí el token en un
hx-headerso hidden input; los POST/PUT/DELETE lo requieren igual que un form normal. - Escapá siempre el HTML del servidor (autoescape de Jinja on).
- No confíes en atributos del cliente: validá en el servidor.
Performance
- Fragmentos chicos y cacheables. Evitá devolver la página entera en cada swap.
- Usá
hx-triggercondelay:para debounce en inputs. hx-boostpara navegación tipo SPA sobre links/forms existentes, con degradación.
DO / DON'T
DO
- Devolver HTML server-rendered; mantener el estado en el DOM.
- Distinguir fragmento vs página completa con
HX-Request. - Escapar salida y proteger CSRF en mutaciones.
- Usar
hx-swap-oobpara actualizaciones multi-zona.
DON'T
- Construir un store de estado en JS paralelo al HTML.
- Devolver JSON a un endpoint HTMX (rompe el modelo hipermedia).
- Olvidar la mejora progresiva / degradación sin JS.
- Insertar HTML sin escapar (XSS).