htmx + Django en vez de un SPA: cuándo es la decisión correcta



Construimos la pantalla de calendario de contenido del panel interno de codeceibe de dos formas: primero esbozada como un módulo de Angular que hablaba con una API JSON, y después, cuando la primera versión empezó a sentirse sobredimensionada, como vistas de Django que devuelven fragmentos HTML que htmx inserta en la página. La segunda versión terminó con más o menos un tercio del código y se entregó en dos días en vez de semana y media. Ese es el caso concreto a favor de htmx para una herramienta interna cargada de CRUD -- pero no es una respuesta universal, y vale la pena ser precisos sobre el porqué.

Qué cambia realmente

Flujo de datos de un SPA frente al de htmxSPA (Angular)Navegador (Angular)Vista de API JSONJS renderiza JSON a DOMhtmx + DjangoNavegador (htmx)Vista DjangoFragmento HTML insertado

La versión en Angular necesita un contrato JSON, un modelo en TypeScript que refleje el modelo de Django, una capa de servicios, y estado del lado del cliente para saber si una fila ya se guardó o no. La versión con htmx necesita una vista de Django que ya existía para la página completa, más un pequeño fragmento de template que esa misma vista también puede devolver. No hay una segunda fuente de verdad sobre cómo se ve una entrada del calendario -- el template de Django es la lógica de render, tanto para la página completa como para el fragmento.

Un ejemplo real

Reprogramar un post desde el calendario es un botón con tres atributos de htmx:

<button hx-post="/panel/calendar/{{ entry.id }}/reschedule/"
        hx-vals='{"date": "2026-11-02"}'
        hx-target="#entry-{{ entry.id }}"
        hx-swap="outerHTML">
  Mover al 2 de nov
</button>

La vista hace lo de siempre en Django -- valida, guarda, y devuelve un fragmento _calendar_entry.html re-renderizado -- y django-htmx le da a la vista un flag request.htmx para que la misma vista pueda devolver la página completa o solo el fragmento según si la petición vino de htmx:

def reschedule(request, pk):
    entry = get_object_or_404(ScheduledPost, pk=pk)
    entry.scheduled_for = parse_date(request.POST["date"])
    entry.save(update_fields=["scheduled_for"])
    template = "panel/_calendar_entry.html" if request.htmx else "panel/calendar.html"
    return render(request, template, {"entry": entry})

Cuándo es la decisión correcta

  • La herramienta es interna y los usuarios son tu propio equipo -- no necesitás una experiencia renderizada del todo en el cliente para justificar el costo de construirla.
  • Las pantallas son fundamentalmente CRUD: listar, filtrar, editar, guardar, con el servidor ya guardando todo el estado relevante en la base de datos.
  • Querés un solo lenguaje de templates y una sola fuente de verdad para el markup, no un template de Django para la página completa más un árbol paralelo de componentes Angular para los mismos datos.
  • Tu equipo es chico y mayormente backend -- los atributos de htmx son una superficie mucho más chica para aprender que todo el ruteo, manejo de estado y pipeline de build de un SPA.

Cuándo no lo es

  • Interacción genuinamente del lado del cliente -- reordenar con drag-and-drop entre muchos elementos, un editor tipo canvas, cualquier cosa con redibujado cuadro a cuadro -- pelea contra el modelo de petición/respuesta de htmx en vez de trabajar con él.
  • Necesitás que la misma lógica de negocio corra offline o dentro de una app móvil. La app Ionic de codeceibe para lectores no puede reutilizar fragmentos de template de Django; esa lógica tiene que vivir en una API real de todos modos.
  • La validación del lado del cliente necesita ser instantánea y compleja (chequeos cruzados entre campos, validaciones async de unicidad con debounce por tecla) -- htmx puede hacer esto con hx-trigger="keyup changed delay:500ms", pero pasado cierto punto estás reimplementando un mini framework de frontend en fragmentos de template y query params.
  • Ya tenés un equipo de frontend dedicado y una librería de componentes -- el costo del SPA ya está pagado, y htmx sería nada más que un segundo patrón para mantener al lado del que ya existe.

El trade-off real

htmx no eliminó el manejo de estado, lo movió al servidor, que es donde las pantallas CRUD de codeceibe ya lo guardaban, en la base de datos. Lo que perdimos es UI optimista e interacción amigable offline; lo que ganamos es un solo template, un solo camino de validación, y ningún contrato JSON que mantener sincronizado con un modelo que cambia cada sprint. Para el calendario de contenido, ese trade-off valió la pena sin dudarlo. Para la app de codeceibe de cara al lector, no valdría -- que es exactamente por qué esa sigue siendo Ionic hablando con una API real.

Dejar un comentario

Los comentarios anónimos se publican tras ser revisados.