carlos / todomvc-bakeoff Public

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

Plain git — no account needed to clone.

Download

Download this file

1# todo-rust
2
3TodoMVC for the CARLOS bakeoff, in Rust. The server renders every page
4and handles every action as a plain form POST, so the app works with
5JavaScript off. A small ES module (`static/app.js`) sends the same POSTs
6with `fetch` and swaps in the updated list, so with JavaScript on it
7behaves like the reference TodoMVC.
8
9Live at [https://todo-rust.bfj.oncarlos.com](https://todo-rust.bfj.oncarlos.com).
10
11## The stack, and why
12
13| Piece | Choice | Why |
14|---|---|---|
15| HTTP server and router | [axum](https://docs.rs/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 |
16| Templates | [maud](https://maud.lambda.xyz) | 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 |
17| SQLite | [rusqlite](https://docs.rs/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 |
18| Allocator | [mimalloc](https://docs.rs/mimalloc) | musl's own allocator is slow when many threads allocate; mimalloc is the usual fix for static musl builds |
19| Randomness | [getrandom](https://docs.rs/getrandom) | 128-bit cookie tokens straight from the OS |
20| Forms and query strings | [serde](https://serde.rs) (through axum's `Form` and `Query`) | |
21
22Flags are parsed by hand (there are three), and cookies too (there is
23one). The shared CSS files are compiled into the binary with
24`include_str!`, so they are served byte for byte.
25
26Lists are kept apart by a `todo_list` cookie holding 128 random bits.
27A cookie the app didn't issue gets a fresh list. Every query names the
28list, so an id from someone else's list is a 404. A POST whose `Origin`
29isn't the app's own is a 403. See `AGENTS.md` for the tenancy rules.
30
31### Layout
32
33- `src/main.rs`: flags, socket or TCP listener, SIGTERM.
34- `src/app.rs`: routes and handlers, including `GET /`.
35- `src/views.rs`: the markup, following `shared/reference.html`.
36- `src/db.rs`: schema, migrations (additive, tracked by `PRAGMA user_version`) and queries.
37- `static/app.js`: the JavaScript layer.
38- `tests/http.rs`: tests that drive the router in-process.
39
40### Without JavaScript
41
42The reference markup has checkboxes and a double-click to edit, neither
43of which can post a form. The page adds a submit button over each
44checkbox (and over the toggle-all arrow) and a link over each title that
45opens `?edit=<id>`. They're transparent and sit exactly on the parts of
46the design they stand in for, so the page looks the same either way. A
47`<noscript>` stylesheet turns them on; with JavaScript they stay
48`display: none`.
49
50## Build, run, test, deploy
51
52Toolchains come from the repo: the Makefile sources `scripts/env.sh`
53itself. Build output goes in `.cache/target` and `dist/`.
54
55```sh
56make build # dist/app for this machine
57make build-arm64 # dist/app-linux-arm64, static (cargo zigbuild, musl)
58make test # cargo test
59dist/app --listen 127.0.0.1:8080 --db "$TMPDIR/rust.db"
60../../scripts/test-app.sh http://127.0.0.1:8080 "$TMPDIR/rust-suite.json"
61../../scripts/boot-check.sh dist/app-linux-arm64
62make deploy MESSAGE="what changed"
63```
64
65`make clean` removes `dist/` and this crate's own compiled files.
66`make clean-deps` removes all of `.cache/target`, so the next build
67recompiles every dependency (the downloaded sources in `~/.cargo` stay).
68
69The version `/api/version` reports is the git short SHA at build time,
70stamped in through the `APP_VERSION` environment variable.
71