carlos / todomvc-bakeoff Public

Clone
git clone https://amadan.net/carlos/todomvc-bakeoff

Plain git — no account needed to clone.

Download

Download this file

todo-rust

TodoMVC for the CARLOS bakeoff, in Rust. The server renders every page and handles every action as a plain form POST, so the app works with JavaScript off. A small ES module (static/app.js) sends the same POSTs with fetch and swaps in the updated list, so with JavaScript on it behaves like the reference TodoMVC.

Live at https://todo-rust.bfj.oncarlos.com.

The stack, and why

| Piece | Choice | Why | |---|---|---| | HTTP server and router | axum 0.8 on tokio (hyper underneath) | The default choice for a Rust web service today. It serves a unix socket and a TCP port through the same axum::serve, and graceful shutdown on SIGTERM is one call | | Templates | maud | HTML written as Rust macros and checked at compile time. Every interpolated value is escaped unless you opt out, which is what you want for user-typed titles | | SQLite | rusqlite with bundled | SQLite is compiled from source into the binary, so there is no shared library to find at run time. One connection behind a mutex, used from tokio's blocking pool | | Allocator | mimalloc | musl's own allocator is slow when many threads allocate; mimalloc is the usual fix for static musl builds | | Randomness | getrandom | 128-bit cookie tokens straight from the OS | | Forms and query strings | serde (through axum's Form and Query) | |

Flags are parsed by hand (there are three), and cookies too (there is one). The shared CSS files are compiled into the binary with include_str!, so they are served byte for byte.

Lists are kept apart by a todo_list cookie holding 128 random bits. A cookie the app didn't issue gets a fresh list. Every query names the list, so an id from someone else's list is a 404. A POST whose Origin isn't the app's own is a 403. See AGENTS.md for the tenancy rules.

Layout

  • src/main.rs: flags, socket or TCP listener, SIGTERM.
  • src/app.rs: routes and handlers, including GET /.
  • src/views.rs: the markup, following shared/reference.html.
  • src/db.rs: schema, migrations (additive, tracked by PRAGMA user_version) and queries.
  • static/app.js: the JavaScript layer.
  • tests/http.rs: tests that drive the router in-process.

Without JavaScript

The reference markup has checkboxes and a double-click to edit, neither of which can post a form. The page adds a submit button over each checkbox (and over the toggle-all arrow) and a link over each title that opens ?edit=<id>. They're transparent and sit exactly on the parts of the design they stand in for, so the page looks the same either way. A <noscript> stylesheet turns them on; with JavaScript they stay display: none.

Build, run, test, deploy

Toolchains come from the repo: the Makefile sources scripts/env.sh itself. Build output goes in .cache/target and dist/.

make build          # dist/app for this machine
make build-arm64    # dist/app-linux-arm64, static (cargo zigbuild, musl)
make test           # cargo test
dist/app --listen 127.0.0.1:8080 --db "$TMPDIR/rust.db"
../../scripts/test-app.sh http://127.0.0.1:8080 "$TMPDIR/rust-suite.json"
../../scripts/boot-check.sh dist/app-linux-arm64
make deploy MESSAGE="what changed"

make clean removes dist/ and this crate's own compiled files. make clean-deps removes all of .cache/target, so the next build recompiles every dependency (the downloaded sources in ~/.cargo stay).

The version /api/version reports is the git short SHA at build time, stamped in through the APP_VERSION environment variable.