---
slug: 43-build-api
title: Construir una API REST
description: Una API REST CRUD completa en Synsema — rutas, auth y validación declarativas, paginación, una base real — en un archivo, en menos líneas que FastAPI, con el servidor de producción integrado.
example_ids: [serve]
---

# 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

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

db_open("./store.db")   -- `require db` da la capability; `db_open` abre la conexión

task check_token(token)   -- la task de auth recibe el bearer token (pelado), no el request
    give when token == "secret" then {"user": "admin"} otherwise nothing   -- devolvé una identidad para permitir, `nothing` para rechazar (401)

serve on 8080
    auth with check_token

    route "GET /products"
        give paged("SELECT id, name, price FROM products ORDER BY id")   -- paginado + 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 automático si no coincide
        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

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

- **Respuestas uniformes** — `ok`/`created`/`fail`/`not_found` llevan el status correcto; un valor pelado se vuelve `200 {json}`.
- **Validación** — `expect body {…}` devuelve `400` con detalle cuando el body no coincide; sin chequeos manuales.
- **Auth** — `auth with <task>` + `requires auth` por ruta.
- **Paginación** — `paged(...)` empuja `LIMIT`/`OFFSET` al SQL y calcula un total exacto.
- **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](/es/0.6.x/40-serve#descubrimiento-lo-que-todo-server-publica).

## 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](/es/0.6.x/20-capabilities) y se [despliega con un flag](/es/0.6.x/71-deploy). Mirá **[Servidor HTTP](/es/0.6.x/40-serve)** para la referencia completa de rutas/SSE/CORS y **[Frontend](/es/0.6.x/41-frontend)** para servir HTML también.
