| 1 | # todo-rust |
| 2 | |
| 3 | TodoMVC for the CARLOS bakeoff, in Rust. The server renders every page |
| 4 | and handles every action as a plain form POST, so the app works with |
| 5 | JavaScript off. A small ES module (`static/app.js`) sends the same POSTs |
| 6 | with `fetch` and swaps in the updated list, so with JavaScript on it |
| 7 | behaves like the reference TodoMVC. |
| 8 | |
| 9 | Live 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 | |
| 22 | Flags are parsed by hand (there are three), and cookies too (there is |
| 23 | one). The shared CSS files are compiled into the binary with |
| 24 | `include_str!`, so they are served byte for byte. |
| 25 | |
| 26 | Lists are kept apart by a `todo_list` cookie holding 128 random bits. |
| 27 | A cookie the app didn't issue gets a fresh list. Every query names the |
| 28 | list, so an id from someone else's list is a 404. A POST whose `Origin` |
| 29 | isn'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 | |
| 42 | The reference markup has checkboxes and a double-click to edit, neither |
| 43 | of which can post a form. The page adds a submit button over each |
| 44 | checkbox (and over the toggle-all arrow) and a link over each title that |
| 45 | opens `?edit=<id>`. They're transparent and sit exactly on the parts of |
| 46 | the 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 | |
| 52 | Toolchains come from the repo: the Makefile sources `scripts/env.sh` |
| 53 | itself. Build output goes in `.cache/target` and `dist/`. |
| 54 | |
| 55 | ```sh |
| 56 | make build # dist/app for this machine |
| 57 | make build-arm64 # dist/app-linux-arm64, static (cargo zigbuild, musl) |
| 58 | make test # cargo test |
| 59 | dist/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 |
| 62 | make 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 |
| 67 | recompiles every dependency (the downloaded sources in `~/.cargo` stay). |
| 68 | |
| 69 | The version `/api/version` reports is the git short SHA at build time, |
| 70 | stamped in through the `APP_VERSION` environment variable. |
| 71 | |