http — server & client
Declare routes on one line, handle uploads and redirects, and call external APIs.
Server
You declare a route on a single line:
use http
http.on :post "/notes" \req -> rep 201 {ok:true}
http.on :get "/notes/:id" \req -> rep 200 {id:req.params.id}
http.serve 8080http.on :method "/path" handler— a route. The method is a symbol (:get,:post,:put,:patch,:del).- The handler is a lambda. Its argument is
req:req.body— the JSON body (automatically converted to a map)req.params.id— the:idin the pathreq.query— query parameters (?key=val)req.headers— headers
rep status body— the response. Ifbodyis a map, it automatically becomes JSON.http.serve port— starts the server.http.serve port {max_body: BYTES}— configures the request body size limit (DoS guard). Default10 MiB(10485760 bytes); over the limit the server returns413 Payload Too Largewithout buffering the body.max_body: 0disables the limit (unlimited — only behind a trusted internal network).
File upload (multipart/form-data)
Files sent by a browser form or curl -F land in req.files, plain form fields
in req.body (symmetric with JSON):
http.on :post "/upload" \req ->
f = req.files.0
fs.write f.filename f.content
rep 201 {saved:f.filename size:f.size}- Each file:
{name filename content size}.contentis a str for UTF-8 text, bytes for binary (image, PDF);sizeis always the byte count. req.filesis always a list — empty when the request is not multipart (eachworks without a nil check).- The
max_bodylimit applies to multipart bodies too.
Redirect
There is no special verb — with rep you give a 302 status and a location key;
it becomes the Location header:
http.on :get "/:code" \req ->
link = db.one "select * from links where code=$1" [req.params.code]
link ?? (rep 404 {error:"not found"})
rep 302 {location:link.url}Route precedence
If two routes overlap (/:code and /stats/:code), the literal (exact) path
automatically wins — regardless of the order written. /stats/:code is always
checked before /:code.
Client
Calling an external API:
res = http.get "https://api.example.com/data"
res = http.post url {key:"val"} # the body becomes JSON automatically
# res.status, res.body, res.headers (a map, keys lowercased)
loc = res.headers.location # or res.headers["content-type"]A redirect (3xx) is not followed by default — res.status is 30x, and
res.headers.location is read. If you need automatic following, add an options
map:
res = http.get url {follow:true} # 3xx → follows Location
res = http.get url {follow:true max:5} # hop limit (default 10)
# res.hops — how many redirects happenedExceeding max is an error. The options map is the last argument:
http.post url body {follow:true}.
Custom request headers
For APIs that require authentication (x-api-key, Authorization,
anthropic-version...), add headers to the options map — this is symmetric with
res.headers in the response:
res = http.post "https://api.anthropic.com/v1/messages" body {
headers: {
"x-api-key": env.ANTHROPIC_API_KEY
"anthropic-version": "2023-06-01"
}
}If a header value is not a string it is converted to text; a header with a nil
value is dropped. If the user provides content-type, that is used instead of the
automatic application/json.