---
name: frontend-htmx
description: |
Axum + Askama + HTMX stack for single-binary web apps.
Use when: building server-rendered UI, lightweight frontends, single deployable binary.
Triggers: "htmx", "askama", "templates", "server-rendered", "single binary frontend".
---
# Frontend: Axum + Askama + HTMX
Single binary web apps. No node_modules, no build pipeline.
## Why This Stack
| Feature | Benefit |
|---------|---------|
| **Askama** | Templates compile into binary, type-checked |
| **HTMX** | 14kb, no JS build, hypermedia-driven |
| **Single binary** | `cargo build --release` → deploy anywhere |
## Dependencies
```toml
# Cargo.toml additions for HTMX frontend
[dependencies]
askama = "0.12"
askama_axum = "0.4"
axum-htmx = "0.6" # HTMX header extractors
tower-http = { version = "0.6", features = ["fs"] } # Static files (dev only)
# HTMX served from CDN or embedded
# https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js
```
## Project Structure
```
src/
├── lib.rs # API + create_app()
├── main.rs # Server entry
├── error.rs # AppError
└── templates/
├── mod.rs # Template structs
├── base.html # Layout with HTMX
├── pages/
│ ├── index.html # Home page
│ └── notes.html # Notes list page
└── partials/
├── note_item.html # Single note (for HTMX swap)
├── note_list.html # Notes list partial
└── note_form.html # Create/edit form
templates/ # Askama looks here by default
└── (symlink to src/templates or copy)
```
## Base Template
```html
{% block title %}App{% endblock %}
{% block head %}{% endblock %}
{% block content %}{% endblock %}
```
## Template Structs (Askama)
```rust
// src/templates/mod.rs
use askama::Template;
#[derive(Template)]
#[template(path = "pages/index.html")]
pub struct IndexTemplate {
pub title: String,
}
#[derive(Template)]
#[template(path = "pages/notes.html")]
pub struct NotesPageTemplate {
pub notes: Vec,
}
#[derive(Template)]
#[template(path = "partials/note_item.html")]
pub struct NoteItemTemplate {
pub note: Note,
}
#[derive(Template)]
#[template(path = "partials/note_list.html")]
pub struct NoteListTemplate {
pub notes: Vec,
}
#[derive(Template)]
#[template(path = "partials/note_form.html")]
pub struct NoteFormTemplate {
pub note: Option, // None for create, Some for edit
}
```
## Page Template
```html
{% extends "base.html" %}
{% block title %}Notes{% endblock %}
{% block content %}
Notes
Loading...
{% endblock %}
```
## Partial Templates
```html
{{ note.title }}
{{ note.content }}
```
```html
{% for note in notes %}
{% include "partials/note_item.html" %}
{% endfor %}
{% if notes.is_empty() %}
No notes yet.
{% endif %}
```
## Handlers
```rust
// src/lib.rs
use askama::Template;
use askama_axum::IntoResponse;
use axum::{
extract::{Path, State},
http::StatusCode,
response::Html,
routing::{delete, get, post},
Form, Router,
};
use axum_htmx::HxRequest;
// Page handler - returns full HTML page
pub async fn notes_page() -> impl IntoResponse {
NotesPageTemplate { notes: vec![] }
}
// Partial handler - returns HTML fragment for HTMX
pub async fn notes_list(State(db): State) -> impl IntoResponse {
let notes = db.get_all_notes().await;
NoteListTemplate { notes }
}
// Create handler - returns new item partial
pub async fn create_note(
State(db): State,
Form(input): Form,
) -> impl IntoResponse {
let note = db.create_note(input).await;
(StatusCode::CREATED, NoteItemTemplate { note })
}
// Delete handler - returns empty (HTMX removes element)
pub async fn delete_note(
State(db): State,
Path(id): Path,
) -> impl IntoResponse {
db.delete_note(id).await;
StatusCode::OK
}
// Conditional: full page vs partial based on HX-Request header
pub async fn smart_notes(
HxRequest(is_htmx): HxRequest,
State(db): State,
) -> impl IntoResponse {
let notes = db.get_all_notes().await;
if is_htmx {
// HTMX request - return partial
NoteListTemplate { notes }.into_response()
} else {
// Full page request
NotesPageTemplate { notes }.into_response()
}
}
pub fn create_app() -> Router {
Router::new()
// Pages
.route("/", get(notes_page))
// Partials (HTMX targets)
.route("/notes/list", get(notes_list))
// API actions
.route("/notes", post(create_note))
.route("/notes/:id", delete(delete_note))
}
```
## HTMX Patterns
### Swap Strategies
| Pattern | `hx-swap` | Use Case |
|---------|-----------|----------|
| Replace content | `innerHTML` (default) | Update container |
| Replace element | `outerHTML` | Update item in list |
| Add to start | `afterbegin` | New items at top |
| Add to end | `beforeend` | New items at bottom |
| Delete | `delete` | Remove element |
### Common Attributes
```html
Loading...