Servidor HTTP
Construir una API REST
Una API CRUD completa — rutas, auth, validación, paginación, una base de datos real — en un archivo, con el servidor HTTP de producción integrado. Sin framework, sin servidor ASGI, sin requirements.txt.
Todo junto§
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})
Correlo: synsema serve api.syn. Eso es un servidor de producción — async, multi-core real, un único binario estático sin runtime.
Lo que obtuviste gratis§
-- 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")- Respuestas uniformes —
ok/created/fail/not_foundllevan el status correcto; un valor pelado se vuelve200 {json}. - Validación —
expect body {…}devuelve400con detalle cuando el body no coincide; sin chequeos manuales. - Auth —
auth with <task>+requires authpor ruta. - Paginación —
paged(...)empujaLIMIT/OFFSETal SQL y calcula un total exacto. - JSON exacto —
json of requestse parsea comojson_decode(engine v0.6.30): un entero de cualquier tamaño llega exacto, nunca redondeado por un float, yNaN/Infinityson un400.expect body {price: number}lo acepta como número. - Valores, no referencias —
bes la copia del body que tiene el handler; una task auxiliar que lo cambia lo devuelve (set b to with_total(b)), como muestran los tests del ejemplo. - Seguro por defecto —
require db/serve, secretos redactados, con scope de capacidad.
Tu API ya tiene OpenAPI y /docs§
Nada que agregar. Con las rutas de arriba el server ya sirve /openapi.json (OpenAPI 3.1 derivado de route + expect + requires auth + rate_limit, con x-synsema-capabilities diciendo qué puede tocar cada operación) y /docs — una página para navegar y probar cada operación desde el navegador; un agente que manda Accept: text/markdown recibe la misma referencia en Markdown. Nombrá la versión con describe version: "1.0.0". En CI, synsema openapi app.syn --out openapi.json escribe el mismo documento sin levantar nada: el spec es un artefacto de build del fuente, nunca una segunda cosa que mantener. Detalles y límites (sin schema de respuesta, sitemap sin rutas paramétricas): Descubrimiento.
vs. FastAPI§
Sin framework que instalar, sin modelos Pydantic, sin Uvicorn, sin requirements.txt — un binario, un archivo. La validación y la auth son keywords del lenguaje, no decoradores que cableás a mano. @app.post + un modelo Pydantic ↔ route "POST /x" + expect body {…}; /docs ↔ /docs; app.openapi() ↔ synsema openapi app.syn. Y es seguro por defecto y se despliega con un flag. Mirá Servidor HTTP para la referencia completa de rutas/SSE/CORS y Frontend para servir HTML también.