Problem
I wanted to write real backend Rust. Routing, typed extractors, proper error handling. 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 Rust application. A REST API with server-rendered pages, backed by PostgreSQL, with enough moving parts to hit real engineering problems.
The main technical choices:
- 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 and 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, with its own toolchain (mise), hooks, and deployment pipeline.
Rust’s type system forces decisions about data shape up front. That is uncomfortable at first. After that, the rest of the code is much harder to get wrong. The compiler catches whole categories of bugs that in other languages show up at runtime or in production.