comptime Value — Handlers Capture Nothing, Shared State Comes In as State[T] — So the Whole Tree Is Checked and Its Matcher Built While the Program Is BuiltStatus: Accepted
Date: October 7, 2026 (#121)
Answers: how routes are grouped under prefixes, across files and packages,
without giving up the checks and the description that need every route at once
Related:
ADR-332 (a handler gets only what its signature declares);
ADR-333 (comptime parameters);
ADR-329 (an API description as a constant);
Part II 10.2 (what a comptime may hold), Part III 17.1
How are routes grouped - /api/v1/…, /admin/… - and kept in the files and
packages they belong to, while conflicts, formats and the API description are
still settled while the program is built?
A check that one route needs is made at its .get. A conflict between two
routes, whether a body’s type fits the formats where its route ends up, and the
description of all routes need the whole tree with its prefixes. A tree that
is a value built while the program runs - mounted in an if, under a prefix
read from a file - is not known while it is built; and a handler that captures
a database handle cannot exist then.
| Groups | Whole-tree checks | Shared state in a handler | |
|---|---|---|---|
| Express | Router, app.use("/api", r) |
at run time, or none | captured |
| axum | Router::nest("/api", r) |
when the router is built, at run time | State<T>, typed |
| FastAPI | APIRouter(prefix=…) |
at run time | Depends(…) |
| Rocket | routes![…], mount("/api", …) |
at launch | &State<T>, typed |
| Phoenix | scope "/api" do … end |
while compiling; the router becomes pattern matching | conn.assigns |
From memory, not checked against a version. What transfers: Phoenix’s router compiled into the program, and state that a handler declares by type.
| Option | Why it lost |
|---|---|
| A group as a block inside one server expression | every route in one file; a package cannot ship its routes |
| A tree built at run time, its whole-tree checks at start-up | two moments for one kind of check; no matcher built ahead |
A run-time tree with a comptime prefix and straight-line composition only |
a rule hard to explain - no mount in an if - that still leaves captured state |
Handlers that capture values from main |
the tree cannot be known while building |
comptime value, mounted under prefixes.// orders.nika
pub comptime ROUTES = http::Routes()
.get("/{id}") fn(id: i64, db: State[Db]) -> Order { … }
.post("/") fn(data: Body[NewOrder], db: State[Db]) -> Order { … }
// main.nika
comptime APP = http::Routes()
.mount("/api/v1/orders", orders::ROUTES)
.mount("/admin", admin::ROUTES; formats: [json::Format])
fn main() throws {
let db = Db::connect(…)
http::Server(APP; formats: [json::Format, form::Format], state: [db]).listen(":8080")
}
A server takes a tree and has no .get of its own. A tree may be built from
others across files and packages, under conditions the build can decide. .mount takes a group’s formats, which replace the
server’s for the routes below it.
comptime may hold a function that captures nothing.Changes Part II 10.2: what an initialiser may hold.
A lambda or a named function whose body reaches nothing from its surroundings
is a build-time value. One that captures is refused with NK1127, whose help
names State[T].
State[T], filled from what the server is given.http::Server(…; state: [db, cache]) hands the values over when the program
runs; a handler or an extractor that declares State[Db] receives db.
state holds one value per type. A State[T] no value of T is given for,
and two values of one type, are refused while the program is built.
Conflicts (ADR-332 D2), a body’s type against the formats where
its route ends up, every State[T] against state:; the API description as a
constant (ADR-329); and a matcher that decides on the path’s
segments, built from the tree rather than searched while the program runs.
main; it declares
State[T], and main hands the value to the server.http’s to choose and to measure.