Problem
I wanted to write real backend Rust — routing, typed extractors, proper error handling — but toy examples never teach the parts that actually matter. I needed a project with real data, real constraints, and a reason to keep coming back to it.
Approach
Build a whisky collection tracker as a full-stack Rust application: a REST API with server-rendered pages, backed by PostgreSQL, with enough moving parts to encounter real engineering problems.
The key technical decisions:
- Axum 0.7 as the web framework, with utoipa for OpenAPI docs (Swagger UI and ReDoc).
- Server-side rendering with Askama templates and Datastar for interactivity — no separate JavaScript frontend.
- sqlx for type-checked SQL queries against PostgreSQL.
- Meilisearch for search indexing, connected via NATS JetStream for async reindexing.
- S3-compatible storage (Hetzner in production) for bottle images, with a separate
image-pipelinebinary that batch-generates AVIF variants at multiple sizes.
Stack
Rust, Axum, Askama, Datastar, sqlx, PostgreSQL, Meilisearch, NATS JetStream, S3, Tailwind CSS + DaisyUI.
Outcome
The API runs in production. It has its own migration-driven schema, test harness, merge queue with CI tiers, and Beads issue tracker. The codebase lives in the same monorepo as this site but has its own toolchain (mise), hooks, and deployment pipeline.
What it taught me: Rust’s type system forces decisions about data shape up front, which is uncomfortable at first and then makes the rest of the code much harder to get wrong. The compiler catches entire categories of bugs that in other languages would surface at runtime or in production.