---
slug: 43-build-api
title: Build a REST API
description: A complete CRUD REST API in Synsema — routes, declarative auth and validation, pagination, a real database — in one file, in fewer lines than FastAPI, with the production server built in.
example_ids: [serve]
---

# 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

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

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

- **Uniform responses** — `ok`/`created`/`fail`/`not_found` carry the right status; a bare value becomes `200 {json}`.
- **Validation** — `expect body {…}` returns `400` with detail when the body doesn't match; no manual checks.
- **Auth** — `auth with <task>` + `requires auth` per route.
- **Pagination** — `paged(...)` pushes `LIMIT`/`OFFSET` into SQL and computes an exact total.
- **Secure by default** — `require db`/`serve`, secrets redacted, capability-scoped.

## 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](/en/0.6.x/40-serve#discovery-what-every-server-publishes).

## 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](/en/0.6.x/20-capabilities) and [deployed with one flag](/en/0.6.x/71-deploy). See **[HTTP server](/en/0.6.x/40-serve)** for the full route/SSE/CORS reference and **[Frontend](/en/0.6.x/41-frontend)** to also serve HTML.
