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-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 cc targeting 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, on asyncdispatch. 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. asynchttpserver doesn't take a unix socket either, but it's a small gap: unixServer in src/app.nim binds the socket itself and gives it to the server through std/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 as Content-Length ones. 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 case on the path. Six actions and four pages don't need a router.
  • Templates: Nim's built-in stdtmpl source 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 through xmltree.escape.
  • SQLite: db_connector's sqlite3 binding (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 loads libsqlite3.so; --dynlibOverride:sqlite3 turns that off so it links against the compiled-in copy. src/store.nim prepares 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_version records how far the file has got).
  • JavaScript: one plain ES module, no framework (static/app.js). It sends the same form POSTs with an X-Fragment: 1 header, 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.css and shared/index.css, embedded at compile time with staticRead and served byte for byte. static/app.css adds 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 caches

Run 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.json

On 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 |