Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Running It for Real

$ zuri run taskboard
2026-09-11T02:29:55+01:00 INFO [taskboard]: task board on http://127.0.0.1:8000

Open http://127.0.0.1:8000 and the board is there. Add a task, move it, delete it. Stop the server with Ctrl+C and start it again, and everything is still there.

The Entry Point

Filename: index.zu

import log
import os

import @.app
import .config

if __root__ == __file__ {
  var server = app.build()

  os.on_signal('INT', @() {
    log.info('shutting down')
    os.exit(0)
  })

  log.info('task board on http://${config.HOST}:${config.PORT}')
  server.listen()
}

import @.app re-exports app, so another program can import .taskboard { app } and build a server of its own. The __root__ == __file__ check means importing this package starts nothing.

os.on_signal('INT', ...) catches Ctrl+C. This application saves after every change, so there is nothing to flush, and the handler is here because the place to put “finish what you were doing” is obvious once the hook exists.

Configuration

Every setting comes from the environment with a default:

$ PORT=9000 HOST=0.0.0.0 DATA_DIR=/var/lib/taskboard zuri run taskboard

Running it with no environment at all works too, which is what makes the first run painless.

Exercising It

$ curl -s localhost:8000/health
{"ok":true,"tasks":0}

$ curl -s -X POST -H 'content-type: application/json' \
    -d '{"title":"write the capstone","notes":"chapter 17"}' \
    localhost:8000/api/tasks
{"id":"01a08e15-...","title":"write the capstone","notes":"chapter 17",
 "column":"todo","created_at":1789090195.9,"created_on":"Sep 11, 2026",
 "is_done":false}

$ curl -s -X POST -d 'title=review+it&column=doing' localhost:8000/tasks -o /dev/null -w '%{http_code} -> %{redirect_url}\n'
302 -> http://localhost:8000/

$ curl -s -X POST -d 'title=&column=todo' localhost:8000/tasks -o /dev/null -w '%{redirect_url}\n'
http://localhost:8000/?error=a+task+needs+a+title

$ curl -s localhost:8000/api/summary
{"todo":0,"doing":1,"done":1}

The HTML form and the JSON API reach the same board through the same validation, and each reports failure in the form its own client understands.

What Is Stored

$ cat taskboard/data/board.json
[
  {
    "id": "01a08e15-e758-741d-b368-d5a988cb90cd",
    "title": "write the capstone",
    "notes": "chapter 17",
    "column": "todo",
    "created_at": 1789090195.9
  }
]

Readable, editable, and greppable. An invalid column typed in by hand is rejected when the board next loads, because from_dict() goes through the same constructor everything else does.

The Single-Process Assumption

server.listen() serves one connection at a time on one isolate. For a team board that is more than enough: the slowest thing in a request is Wire’s first compile, and everything after it is single-digit milliseconds.

Zuri can serve across cores, and http.serve() is how. It takes a setup function, calls it once inside each worker with that worker’s own server, and runs the pool:

import http
import .app

# In app.zu, alongside build():
#
#   def setup(server) {
#     ...register the same routes and middleware here...
#   }

http.serve(app.setup, { port: 8000, workers: 4 })

Doing it here would change one thing that matters: each isolate has its own heap, so each worker would have its own Board, its own copy of every task, and its own idea of what board.json should contain. Two workers saving at once would each write a complete file, and one of them would win.

The board would need to stop being in-memory state. The choices are the usual ones:

  • A database. Move _json_store.zu to a real store with transactions. Nothing above it changes; that is why it is one private file.
  • One owner. Keep the board in a single isolate and have the workers talk to it over a channel. Every mutation becomes a message, and one isolate serialises them.
  • A lock. Guard the file with an advisory lock and re-read before every write. Simplest to add, and it makes every request pay for the file.

Which one is right depends on how many people are using the board, and none of them is worth doing before the answer is “more than this can handle.” The design already leaves the door open, and that is the part to get right early.

What This Application Used

Almost all of it.

ChapterWhat the application uses it for
3, 4every line
5anonymous handlers, closures over board, typed parameters
6Task, Board, custom errors, @to_json, to_string
7TaskError, NotFoundError, catch at the form boundary
8packages, index.zu, @ re-export, _ privacy, __file__
9file(), atomic rename, os.join_paths, os.get_env
12the HTTP server, routing, middleware, static files
13json, uuid, date, log
14Wire: layout, slots, loops, filters, escaping
16the request log, and the error shapes that make it readable
19the suite in Testing the Board

What it did not use is as informative. There are no isolates, because one process is enough. There is no validate, because the rules belong to the model. There is no binary handling, no compression, no reflection. Those are all available, and reaching for them here would have made the application longer without making it better.

Where to Take It

The natural next steps, roughly in order of how much they teach:

Authentication. bcrypt for password hashing, a signed cookie for the session, a middleware that rejects anything unauthenticated. The middleware slot is already there.

Live updates. http.sse pushes an event to the browser when the board changes, so two people looking at it see each other’s edits.

Per-user boards. One board.json per user, a Board cache keyed by user id, and path becoming a parameter rather than a default.

A database. Replace _json_store.zu, change nothing else, and watch the underscore earn its keep.

Search and filtering. filter() over the board, exposed as query parameters, with the same code serving the HTML page and the API.

Every one of those is a change to one layer. That is what the layout was for, and Testing the Board is how you make any of them without holding your breath.