Tour: JSON API

Rex handlers that return objects or arrays get automatically serialized to JSON with content-type: application/json. Combined with the KV store, this gives you a complete CRUD API in a few lines of Rex.

Endpoints

GET /api/articles — list all articles

POST /api/articles — create article

GET /api/articles/:slug — get one article

PUT /api/articles/:slug — update article

DELETE /api/articles/:slug — delete article

All endpoints require an Authorization header (enforced by api/_middleware.rex).

Try It

Step 1: Seed an API key
sqlite3 examples/knowledge-base/data.db "INSERT INTO kv VALUES('keys:demo','1')"
Step 2: Create an article
curl -X POST http://localhost:4000/api/articles \
  -H 'Authorization: demo' \
  -d '{"slug":"hello","title":"Hello World","body":"# Hello

Created via API."}'
Step 3: List articles
curl http://localhost:4000/api/articles -H 'Authorization: demo'
Step 4: Try without auth (expect 401)
curl http://localhost:4000/api/articles

How It Works

Rex's existence-based semantics make request handling natural. when checks if a value is defined (not just truthy), so when method == "GET" branches cleanly. Return an object literal and the server serializes it:

when method == "GET" do
  articles = db.list("article:")
  items = [json.parse(a.value) for a in articles]
  {ok: true, articles: items}
end

Database

The db.* opcodes provide a simple key-value store backed by SQLite. The database file is created automatically on first run.

articles.rex Source

/* Article collection: GET list, POST create */
when method == "GET" do
  articles = db.list("article:")
  items = [ json.parse(a.value) for a in articles ]
  return { ok: true articles: [ { slug: a.slug title: a.title updated: a.updated } for a in items ] }
end

when method == "POST" do
  input = json.parse(body)

  unless input.slug and input.title and input.body do
    res.status = 422
    return { ok: false error: "slug_title_body_required" }
  end

  record = {
    slug: input.slug
    title: input.title
    body: input.body
    created: time.now()
    updated: time.now()
  }
  db.set(`article:${input.slug}`, json.stringify(record))
  res.status = 201
  return { ok: true slug: input.slug }
end

res.status = 405
{ ok: false error: "method_not_allowed" }

articles/[slug].rex Source

/* Single article: GET, PUT, DELETE */
slug = params.slug
unless slug do
  res.status = 400
  return { ok: false error: "missing_slug" }
end

when method == "GET" do
  record = db.get("article:" + slug)
  unless record do
    res.status = 404
    return { ok: false error: "not_found" }
  end
  return { ok: true article: json.parse(record) }
end

when method == "PUT" do
  input = json.parse(body)
  existing = db.get("article:" + slug)

  unless existing do
    res.status = 404
    return { ok: false error: "not_found" }
  end

  old = json.parse(existing)
  updated = {
    slug: slug
    title: input.title or old.title
    body: input.body or old.body
    created: old.created
    updated: time.now()
  }
  db.set("article:" + slug, json.stringify(updated))
  return { ok: true slug: slug }
end

when method == "DELETE" do
  db.del("article:" + slug)
  return { ok: true deleted: slug }
end

res.status = 405
{ ok: false error: "method_not_allowed" }