carlos / todomvc-bakeoff Public
đŸ¤– todo-nim
TodoMVC in Nim for the CARLOS bakeoff: server-rendered, works with JavaScript off, and keeps each visitor's list in SQLite. It runs at https://todo-nim.bfj.oncarlos.com.
The stack, and why
- Nim 2.2, compiled to C. Nim builds a small native binary, and its C backend makes the static linux-arm64 build easy:
zig cctargeting musl acts as the C compiler and linker, and the result is a 1.7 MB static executable that starts in milliseconds. - HTTP: the standard library's
asynchttpserver, onasyncdispatch. A todo list doesn't need more than a single-threaded event loop, and the standard library means no framework to pin. The popular alternatives don't fit this platform: Mummy and httpbeast (Jester's server) only listen on TCP, and Carloku hands the app a unix socket.asynchttpserverdoesn't take a unix socket either, but it's a small gap:unixServerinsrc/app.nimbinds the socket itself and gives it to the server throughstd/importutils.privateAccess. The server is a copy of the standard library's (src/httpserver.nim) with one fix: upstream doesn't limit chunked request bodies, so one POST could exhaust the instance's memory. The copy holds chunked bodies to the same 1 MiB limit asContent-Lengthones. The standard library's docs call this server basic; it has no request timeouts or keep-alive limits. Carloku's edge sits in front of it, and that's an acceptable trade for an app this size. - Routing: a
caseon the path. Six actions and four pages don't need a router. - Templates: Nim's built-in
stdtmplsource filter (src/page.nimf). It compiles the template into an ordinary Nim proc, so the markup is type-checked and there's nothing to load at run time. Titles go throughxmltree.escape. - SQLite:
db_connector'ssqlite3binding (the database module that moved out of Nim's standard library) over the SQLite 3.54.1 amalgamation, compiled into the binary. The binding normally loadslibsqlite3.so;--dynlibOverride:sqlite3turns that off so it links against the compiled-in copy.src/store.nimprepares every statement once and binds parameters, so no user input is ever spliced into SQL. WAL mode,synchronous = NORMAL, and numbered migrations that only add (PRAGMA user_versionrecords how far the file has got). - JavaScript: one plain ES module, no framework (
static/app.js). It sends the same form POSTs with anX-Fragment: 1header, and the server answers with the re-rendered list section instead of a 303. Rendering stays in one place, the server. - The CSS is
shared/base.cssandshared/index.css, embedded at compile time withstaticReadand served byte for byte.static/app.cssadds the no-JavaScript controls and hides them once JavaScript is running.
Lists are anonymous: the first visit gets a cookie holding 128 random bits from the OS (std/sysrand), and every query is scoped to it. See AGENTS.md for the tenancy declaration.
Build, run, test, deploy
Put the toolchains on PATH first: source scripts/env.sh from the repo root. The first build fetches db_connector into .cache/nimble and the SQLite amalgamation into .cache/dl, checking it against a pinned SHA-256.
make build # dist/app for this machine
make build-arm64 # dist/app-linux-arm64, static (zig cc, musl)
make test # unit tests: the store, and the routes with fake requests
make clean # removes dist/
make clean-deps # also removes compiled C: .cache/nimcache-* and zig's cachesRun it locally and drive the shared behaviour suite against it:
dist/app --listen 127.0.0.1:8080 --db $TMPDIR/nim.db
scripts/test-app.sh http://127.0.0.1:8080 $TMPDIR/nim-suite.jsonOn Carloku it runs as app --socket <path> --db <path>, reads CARLOS_HOST for its origin, and sets Secure on its cookie. It exits cleanly on SIGTERM.
/api/version reports the git short SHA the binary was built from, so commit before building what you deploy. Then:
scripts/boot-check.sh dist/app-linux-arm64
make deploy MESSAGE="what changed"Layout
| Path | What | |---|---| | src/app.nim | Flags, the server, routes and cookies | | src/httpserver.nim | The standard library's HTTP server, with chunked bodies limited | | src/store.nim | SQLite: schema, migrations, every query | | src/views.nim, src/page.nimf | The page and the list fragment | | static/app.js, static/app.css | The JavaScript layer and the no-JS controls | | tests/test_app.nim | Unit tests |