ADR-334: A Tree of Routes Is a 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 Built

Status: 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

1. Question

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.

2. How others do it

  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.

3. Options, weighed

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

4. Decision

D1 — A tree of routes is a 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.

D2 — A 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].

D3 — Shared state is an extractor, 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.

D4 — The whole tree is checked, described and compiled 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.

5. Consequences

6. What this does not decide

This site is open source. Improve this page.