carlos / todomvc-bakeoff Public
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, includingGET /.src/views.rs: the markup, followingshared/reference.html.src/db.rs: schema, migrations (additive, tracked byPRAGMA 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.