Build full-stack stateful web apps using Axum + HTMX + Alpine.js + Neon (PostgreSQL).
Stack: Axum + SQLx (Rust), Askama templates + HTMX + Alpine.js + PicoCSS, Neon (PostgreSQL), Docker.
Workflow
Phase 1: Setup
Prerequisites (install once):
npm i -g neonctl brew install jq cargo install sqlx-cli --features postgres,native-tls
Required env vars: NEON_API_KEY , NEON_PROJECT_ID
Scaffold app:
NEON_BRANCH_TTL=2h .claude/skills/rust-webapp/scripts/scaffold <app-name> .
Creates app files and a Neon branch ({app}-dev ) with 2h expiration.
Phase 2: Data Modeling
-
Define models in src/models.rs
-
Write migration SQL in migrations/001_init.sql
-
Use SERIAL for i32, BIGSERIAL for i64
Reference: Models - SQLx patterns, struct definitions, type mapping
Phase 3: Backend Implementation
-
Add route handlers in src/main.rs
-
All handlers return Result<T, AppError>
-
use ? operator
-
NEVER use .expect() or .unwrap()
-
causes server crashes
-
Route params use {id} syntax (NOT :id )
Reference: Handlers - CRUD patterns, router setup, transactions
Phase 4: Frontend
-
Update Askama templates in templates/
-
Delete unused template files (create.html, edit.html if not needed)
-
Use HTMX for interactivity, Alpine.js for state
Reference: Templates - Askama, HTMX, Alpine patterns Reference: Design - CSS components, layout patterns
Phase 5: Validation
Validate (runs cargo check, clippy, tests, release build):
.claude/skills/rust-webapp/scripts/validate .
Fix all errors before completing.
Template Structure
├── Cargo.toml # Dependencies ├── Dockerfile # Multi-stage Rust build ├── src/ │ ├── main.rs # Axum server, routes, templates │ ├── db.rs # SQLx pool setup │ └── models.rs # Data structs ├── templates/ │ ├── base.html # Base layout (PicoCSS/HTMX/Alpine CDN) │ ├── index.html # List view │ ├── edit.html # Edit form │ └── create.html # Create form ├── static/ │ └── styles.css # Custom CSS overrides └── migrations/ └── 001_init.sql # Database schema
Critical Rules
-
NEVER .expect() or .unwrap() in handlers - use ? operator
-
Route params: {id} not :id
-
wrong syntax compiles but panics at runtime
-
All handlers return Result<T, AppError>
-
Check rows_affected() for DELETE/UPDATE to return 404
-
Use compile-time SQLx macros (query! , query_as! )
-
Ensure the app has enough logs for basic observability
Full list: Pitfalls
Constraints
-
Neon (PostgreSQL) required - needs DATABASE_URL
-
All routes at root level (/, /new, /{id}/edit)
-
Strict clippy lints - must pass validation