Synsemadocsv0.6.xENES

HTTP server

HTTP server

A native, production HTTP server — no framework to add (async hyper/tokio). Everything is deny-by-default, so a server needs require serve(port).

serve.syn
-- Doc example: the serve response contract. Helpers return {status, value}; the
-- runtime renders them. (A real `serve on` block doesn't terminate, so the doctest
-- asserts the response shapes the handlers give — see the prose for a full server.)
intent: "doc example: serve response contract"

print("ok → " + text(status of ok({"a": 1})) + ",  fail(400) → " + text(status of fail(400, "bad")))

test "uniform response helpers carry a status + value"
    assert_eq(status of ok({"a": 1}), 200)
    assert_eq(status of created({"id": 1}), 201)
    assert_eq(status of fail(400, "bad input"), 400)
    assert_eq(status of not_found("missing"), 404)
    assert_eq((value of fail(400, "bad input"))["error"], "bad input")

Routes & params§

require serve(8080)

serve on 8080
    route "GET /products"
        give sql("SELECT id, name, price FROM products")
    route "GET /products/:id"
        give sql("SELECT * FROM products WHERE id = ?", [params.id])
    route "GET /files/*path"            -- catch-all (variable depth)
        give read_file(params.path)

Routes match by specificity (exact > :param > *catchall), not declaration order.

Auth & validation§

serve on 8080
    auth with check_token
    route "POST /products" requires auth
        expect body {name: text, price: number}    -- 400 if it doesn't match
        give created(json of request)

The auth task receives the bearer token; declare it with 2 parameters (task check(token, request)) and it also receives the request map — that's what unlocks cookie sessions (Login & sessions).

The request & responses§

request has .method .path .body .json .form .headers .cookies .query .params .user .ip .body_file (.cookies — engine v0.5.5+; .form — engine > v0.5.9). form of request is the parsed form body: urlencoded → {field: text}; multipart → text fields plus file uploads as {filename, content_type, data} (exact bytes); no form body → empty map — classic <form method="post"> posts need no fetch/JSON. .headers, .query and .params are maps — index by key (request.headers["authorization"]; header names are lowercased). Indexing a missing key errors, so guard optional ones with contains(request.headers, "x"). query and params are also bound as bare locals, so params.id works directly. When a large body spills to disk (over ~1 MiB), .body is empty and .body_file is a temp-file path — read it with read_body() / read_body_bytes().

Handler scope: request, query and params live only in the handler's own scope — a task the handler calls does not see them. Pass what the task needs as an argument (e.g. lookup_user(request)), never a bare request referenced inside that task.

Responses use the uniform helpers — ok(x), created(x), fail(code, msg), not_found(msg), respond(text, content_type), redirect(url) — or give a value directly. Any of them can carry extra headers or cookies: with_header(resp, name, value) / set_cookie(resp, name, value, opts?) — see Login & sessions.

Shared state across requests (state_*)§

A set on a global inside a handler does not persist to the next request — each request runs on its own snapshot of the globals. State shared across requests/handlers lives in an in-memory store with the life of the server: state_set(key, value), state_get(key, default?), state_incr(key, delta?), state_delete(key), state_all() (a map snapshot of every key — {"a": 1, "hits": 2}). Gone on restart; for durable state use a database or the declared memory (Memory & state).

Built in§

See Frontend for HTML pages and Build a website for the end-to-end walkthrough.