[Web]AppThe framework core: routing DSL, middleware onion, render conventions, and error mapping over a pure handler pipeline — the [HTTP]Server is merely the transport.
use std:net/http_server; use std:web/*; var app = [Web]App.new; app.get:'/' do:{ 'hello from Quoin' }; app.get:'/users/:id' do:{ |req| users.at:(req.param:'id') }; "* Map -> JSON app.use:{ |req next| next.value:req }; "* middleware app.serve:':8080' "* start + join (blocks)
Handler blocks may take the request as a parameter ({ |req| … }) or address it as self ({ .param:'id' }); the return value is normalized by the respondTo: conventions. app.handle:req is the whole app as a pure function — request in, response out, no sockets — so apps unit-test in-process. See docs/internal/WEB_ARCH.md.
Include error detail in 500 bodies (a development aid — off by default).
Register handler h for DELETE requests matching pattern; returns self.
The transport-side hop: debug observability is answered HERE (the main VM — its psTree sees the whole topology: connection tasks, pool workers, per-worker states), request logging too (it sees request, response, and elapsed time in both the in-VM and pool paths, and the pure handle: stays silent for unit tests), and everything else goes to the pool or the in-VM pipeline.
Register handler h for GET requests matching pattern (':name' and '*splat' segments capture into req.params — see [Web]Route); returns self.
app.get:'/users/:id' do:{ |req| users.at:(req.param:'id') }
Run one request through the whole app — middleware onion -> router -> handler -> respondTo: normalization — with HttpError mapped to its status (+ optional body) and anything else to a 500. Always returns an [HTTP]ServerResponse; never throws. Pure (no sockets), so an app unit-tests in-process:
var app = [Web]App.new; app.get:'/hello/:name' do:{ |req| 'hi ' + (req.param:'name') }; var req = [HTTP]ServerRequest.new:{ var method = 'GET'; var target = '/hello/qn' }; (app.handle:req).body.asString "* -> hi qn
Internal: an empty router and middleware chain, debug off, request logging on.
Log each SERVED request through Log.info: (on by default): method, target, status, elapsed time — e.g. GET /users/7 -> 200 (1ms 200µs). Lives on the transport hop, so the pure handle: stays silent (unit tests don't log); Log.level:/Log.sink: govern the lines like any other entry. logRequests:false turns it off; returns self.
Register handler h for an arbitrary verb (e.g. 'OPTIONS'); returns self.
Register handler h for PATCH requests matching pattern; returns self.
The [Web]Pool when serving in pool mode, else nil.
Declare this VM a pool HOST even though it runs inside a worker; returns self. Worker.worker? alone conflates "pool worker" with "any worker": a worker that legitimately HOSTS a pool (the soak spawned from the REPL, any nested-fleet unit) must declare itself with poolMain:true before its serve call, or that call would park waiting for a pool sentinel that is never coming.
Stop the worker pool, if any; returns self.
The worker side of the same-unit model: verify the pool sentinel (pre-buffered at spawn — its absence means this worker was NOT spawned by a web pool, e.g. someone ran serve:workers: inside their own worker), then serve request data until stopped. Each request runs as its own Task, so handlers doing I/O overlap within a worker exactly as connection tasks do in the single-VM mode.
The sentinel-verified entry, for callers that already received the worker's first message (e.g. a mode-dispatching unit like the web soak, which answers 'soak' itself and forwards anything else here).
Register handler h for POST requests matching pattern; returns self.
Register handler h for PUT requests matching pattern; returns self.
Normalize a handler's return value into an [HTTP]ServerResponse (a type-directed multimethod): String -> text/plain, Map/List -> JSON, Integer -> a bare status, Bytes -> application/octet-stream, Generator -> a chunked stream, a response passes through, nil -> 404, anything else -> the text of its .s. Add app-specific variants by subclassing [Web]App and extending the multimethod.
One routed dispatch (the innermost layer of the onion): bind the most specific route's params, run its handler, normalize. A path other verbs serve -> 405 with Allow; an unroutable path -> 404; a malformed percent escape in the path -> 400.
The [Web]Router holding the per-verb route tables.
Serve address on the calling task, blocking: start: then join.
Blocking pool serve: start:workers: (thread-backed) then join.
Blocking pool serve with an explicit backing ('thread' | 'process'): start:workers:backing: then join.
Bind address (e.g. ':8080'; ':0' for ephemeral) and serve in the background, returning the [HTTP]Server handle (port / stop / join / close — tests, graceful shutdown).
start: with the pure pipeline on a pool of n worker isolates, thread-backed (see start:workers:backing:).
start: with the pure pipeline running on a pool of n worker isolates (docs/internal/CONCURRENCY_ARCH.md §13): the transport VM keeps the sockets and ships requests as data; each pool worker re-runs THIS app's unit (VM.unit — the same-unit provisioning model), whose serve call lands in poolWorkerLoop below instead of binding. backing is 'thread' or 'process' — 'process' escapes the same-process scheduling ceiling for CPU-heavy handlers. Constraints, by design: handlers in pool mode cannot capture main-VM mutable state (isolates share nothing), and Generator bodies materialize. workers:0 = the single-VM path, unchanged; needs a unit-run program (throws under the REPL / -e).
Install middleware mw; returns self. Onion model: the first use: is outermost. mw is called with (req, next) — a two-param block, or anything responding to valueWithArgs: — where next is a one-arg block producing the inner response. Returning without calling next short-circuits; either way the return value goes through the respondTo: conventions.