ADR-0011: Server-Side Rendering with HTMX¶
Status: Accepted (decision); partially superseded by ADR-0023 on the template-engine choice Date: 2026-01-01
Status note (2026-05-25): The core decision (server-side rendering + HTMX, no SPA framework) is unchanged and load-bearing. The specific template engine — Jinja2 in the original ADR — was retired in #1042 (v0.67.92) and replaced by the typed Fragment substrate documented in ADR-0023. Surfaces declare
render: fragmentin DSL; the runtime emits HTML from a frozen-dataclass primitive tree. Read the rest of this ADR as the rationale for SSR + HTMX; read ADR-0023 for the rationale on how SSR is implemented today.
Context¶
Dazzle generates live applications directly from DSL specifications. The UI layer must be produced programmatically from DSL constructs (surfaces, fields, modes) without requiring a separate frontend build pipeline or framework expertise.
Early prototypes used a React SPA approach, which introduced:
- Build complexity — Node.js, bundlers, and package management alongside Python
- Two-repo tension — Frontend and backend as separate concerns with separate deployment cycles
- DSL impedance mismatch — DSL-generated UI required serialising full app state to JSON for the client
- Security surface — Client-side routing and token handling added XSS and CSRF exposure
Decision¶
Adopt FastAPI for the API layer, Jinja2 for server-side HTML rendering, and HTMX for interactivity. No JavaScript SPA framework in the main application.
Why SSR with Jinja2?¶
| Criterion | SSR + Jinja2 | React SPA | Vue SPA |
|---|---|---|---|
| DSL → UI generation | Direct template render | JSON serialisation step | JSON serialisation step |
| Build step required | No | Yes | Yes |
| Python-native | Yes | No | No |
| Security defaults | Strong (CSRF built-in) | Manual | Manual |
| Time to first byte | Fast | Slow (JS parse + hydrate) | Slow |
| LLM-generated code quality | High | Variable | Variable |
Why HTMX?¶
HTMX replaces the interactivity layer without introducing a JavaScript framework:
- Partial page updates via
hx-get,hx-post— server returns HTML fragments - No client state — all state lives on the server, consistent with DSL-first design
- Progressive enhancement — pages work without JS, enhanced with it
- Minimal JS — no bundler, no
node_modules, no transpilation
Architecture¶
HTMX triggers return rendered HTML partials from the same Jinja2 layer. No JSON API needed for UI interactions.
Consequences¶
Positive¶
- No frontend build step —
dazzle servestarts immediately - DSL constructs map directly to template variables and partials
- CSRF and XSS protections apply uniformly at the FastAPI layer
- Single language (Python) for all application logic
- LLM-generated templates are straightforward Jinja2, not JSX or component trees
Negative¶
- Rich client interactions (drag-and-drop, offline) require custom JS beyond HTMX
- Team members with SPA backgrounds face a learning curve
- Browser history and deep-linking require explicit HTMX push-url configuration
Neutral¶
- Static assets (CSS, icons) served from
dazzle_ui/static/without a bundler - JavaScript islands remain available for complex widgets via
<script>tags
Alternatives Considered¶
1. React SPA¶
Separate React frontend consuming a JSON API from FastAPI.
Rejected: Adds Node.js build toolchain, complicates DSL → UI code generation, increases security surface.
2. Vue SPA¶
Same architecture as React with Vue instead.
Rejected: Same drawbacks as React. No meaningful advantage for a DSL-generated UI.
3. Separate Frontend and Backend Repositories¶
Independent deployment of UI and API with a CDN in front.
Rejected: Operational overhead without benefit at current scale. DSL spec ties UI and API together by design.
Implementation¶
Templates live in src/dazzle/ui/templates/. HTMX is loaded from CDN or vendored into src/dazzle/ui/static/. FastAPI route handlers in src/dazzle/back/ return TemplateResponse objects backed by Jinja2.