Synsemadocsv0.6.xENES

HTTP server

Build a REST API

A full CRUD API — routes, auth, validation, pagination, a real database — in one file, with the production HTTP server built in. No framework, no ASGI server, no requirements.txt.

The whole thing§

require serve(8080)
require db("./store.db")

db_open("./store.db")   -- `require db` grants the capability; `db_open` opens the connection

task check_token(token)   -- the auth task receives the bearer token (stripped), not the request
    give when token == "secret" then {"user": "admin"} otherwise nothing   -- give an identity to allow, `nothing` to reject (401)

serve on 8080
    auth with check_token

    route "GET /products"
        give paged("SELECT id, name, price FROM products ORDER BY id")   -- paginated + total

    route "GET /products/:id"
        let rows be sql("SELECT * FROM products WHERE id = ?", [params.id])
        give when length(rows) == 0 then not_found("no such product") otherwise rows[0]

    route "POST /products" requires auth
        expect body {name: text, price: number}      -- 400 automatically if it doesn't match
        let b be json of request
        sql_exec("INSERT INTO products (name, price) VALUES (?, ?)", [b["name"], b["price"]])
        give created(b)

    route "DELETE /products/:id" requires auth
        sql_exec("DELETE FROM products WHERE id = ?", [params.id])
        give ok({"deleted": params.id})

Run it: synsema serve api.syn. That's a production server — async, true multi-core, a single static binary with zero runtime.

What you got for free§

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")))

-- A helper a handler calls: it gets a COPY of the map, so it gives the change back.
task with_total(order)
    set order["total"] to order["qty"] * order["price"]
    give order

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")

test "the other forms of fail, not_found, respond and redirect"
    assert_eq(status of fail("bad"), 400)
    assert_eq(value of fail("bad"), {"error": "bad", "status": 400})
    assert_eq(value of fail(503), {"error": "error", "status": 503})
    assert_eq(value of not_found({"hint": "try /v2"}), {"hint": "try /v2"})
    assert_eq(status of respond("", "text/plain", 202), 202)
    assert_eq(status of redirect("/login"), 301)
    assert_eq(status of redirect("/login", 303), 303)
    assert_error(() => redirect("/login", 200))

test "a map a handler passes on is a value: the change comes back with give"
    let b be {"qty": 2, "price": 5}
    let priced be with_total(b)
    assert_eq(priced["total"], 10)
    assert(not contains(b, "total"))          -- the caller's map did not change

test "request.json reads the body like json_decode"
    let bom be decode(bytes([239, 187, 191]))    -- a byte order mark, as some clients send it
    let body be json_decode(bom + "{\"id\": 18446744073709551616, \"v\": -0}")
    assert_eq(body["id"], 2 ** 64)            -- exact, not a float
    assert_eq(text(body["v"]), "0")           -- -0 reads as 0
    assert_error(() => json_decode("{\"v\": NaN}"))   -- a request with it is a 400

test "optional headers: get with a default instead of indexing"
    let headers be {"content-type": "application/json"}
    assert_eq(get(headers, "x-api-key", nothing), nothing)
    assert_eq(get(headers, "content-type", nothing), "application/json")

Your API already has OpenAPI and /docs§

Nothing to add. With the routes above the server already serves /openapi.json (OpenAPI 3.1 derived from route + expect + requires auth + rate_limit, with x-synsema-capabilities saying what each operation may touch) and /docs — a page to browse and try every operation from the browser; an agent that sends Accept: text/markdown gets the same reference as Markdown. Name the version with describe version: "1.0.0". In CI, synsema openapi app.syn --out openapi.json writes the same document without starting anything: the spec is a build artifact of the source, never a second thing to maintain. Details and limits (no response schema, sitemap without parametric routes): Discovery.

vs. FastAPI§

No framework to install, no Pydantic models, no Uvicorn, no requirements.txt — one binary, one file. Validation and auth are language keywords, not decorators you wire up by hand. @app.post + a Pydantic model ↔ route "POST /x" + expect body {…}; /docs ↔ /docs; app.openapi() ↔ synsema openapi app.syn. And it's secure by default and deployed with one flag. See HTTP server for the full route/SSE/CORS reference and Frontend to also serve HTML.