# {{capProjectName}} {{capAppName}} — public REST API

A turnkey **public REST API** built on `defineRestRoute`, with an
auto-generated **OpenAPI 3.1 spec + Swagger UI**. Unlike the reactive
rpc/WebSocket surface (`api-backend`), these are plain raw-HTTP endpoints a
third party calls with a URL + JSON.

## What it demonstrates

- **`defineRestRoute`** — four endpoints over a `products` catalog:
  - `GET    /v1/products?limit=&cursor=` — public, cursor-paginated list
  - `GET    /v1/products/:id` — public, one product or `404`
  - `POST   /v1/products` — **scope-guarded** + **idempotent** create
  - `DELETE /v1/products/:id` — **scope-guarded** delete
- **Query string + path params + JSON body** parsing → one decoded `input`.
- **Scope guards** — `requireScope('products:write')` returns `403` unless the
  caller's API key carries the scope.
- **`Idempotency-Key`** — a retried `POST` replays the first response instead
  of inserting twice (`app.config.ts` `idempotency: true`).
- **`@voltro/plugin-openapi`** — `GET /openapi.json` (the spec) + `GET /docs`
  (Swagger UI), generated from the route descriptors. No hand-written docs.

REST routes are the ONE primitive that is **not auto-discovered** — they're
registered explicitly via `restRoutes` in `app.config.ts`.

## Run it

```bash
voltro dev .
```

Boots on `http://localhost:4000`. Then:

```bash
# The spec + interactive docs
open http://localhost:4000/docs
curl -s http://localhost:4000/openapi.json | jq '.info, (.paths | keys)'

# Public reads — no auth
curl -s http://localhost:4000/v1/products            # → { "data": [], "nextCursor": null }
curl -s http://localhost:4000/v1/products/prod_xyz   # → 404

# Guarded create — the demo API key carries products:write
curl -s -X POST http://localhost:4000/v1/products \
  -H 'Authorization: Bearer restdemo_devkey' \
  -H 'Content-Type: application/json' \
  -d '{"name":"Widget","priceCents":1999}'           # → 201 the created product

# Same call WITHOUT the key → 403 (the guard rejects)
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://localhost:4000/v1/products \
  -H 'Content-Type: application/json' -d '{"name":"x","priceCents":1}'   # → 403

# Idempotency — repeat with the same key header → the SAME response replayed
curl -s -X POST http://localhost:4000/v1/products \
  -H 'Authorization: Bearer restdemo_devkey' \
  -H 'Idempotency-Key: order-42' \
  -H 'Content-Type: application/json' \
  -d '{"name":"Once","priceCents":500}'
```

## Auth — the demo key is DEV-ONLY

`app.config.ts` ships ONE hardcoded API key (`restdemo_devkey` → scopes
`products:read` + `products:write`) so the guarded routes work on first boot.
The `apiKeyStrategy` SHA-256-hashes the bearer token and matches the hash to a
fixed Subject.

**Before you deploy**, delete the demo constant and do one of:

- look the token hash up against your own key store (store key **hashes**,
  never raw tokens), or
- flip on the framework's first-class API keys — set `apiKeys: true` in
  `app.config.ts` for admin-gated issue/list/revoke against `_voltro_api_keys`,
  with hash-only storage built in.

## Make it real

- `store: 'memory'` → `store: 'postgres'` (data survives restarts).
- Replace `products` in `database/schema.ts` with your domain; add `tenant()`
  if the API is multi-tenant (then scope each handler by the key's `tenantId`).
- Add routes — drop another `*.route.tsx`, `export default defineRestRoute(…)`,
  and add it to the `routes` array in `app.config.ts` (it joins both the mount
  and the OpenAPI spec automatically).
- Version/deprecate — a descriptor's `deprecated` sets a `Deprecation: true`
  header; `sunset` sets `Sunset:` and `410`s past the date.
