Chapter 45 / 51
Volume 5 · Extensions, Ops & Projects
Project 1: a REST API service (a todo backend)
One chapter to string the first four volumes together: a REST service with persistence, auth, pagination and hot reload — from empty directory to acceptance.
Overview
Positioning: the first of three projects, and the most universally useful. Recommended: actually build it — each section ends with an acceptance point; passing them all completes the project. Reference: demo-single's app.h/item.h is the mature form of exactly this project.
45.1 Project goal
Build a "todo" backend (any resource works the same):
| Capability | Endpoints | Volumes used |
|---|---|---|
| Resource CRUD | GET/POST /api/v1/todos, GET/PUT/PATCH/DELETE /api/v1/todos/{id} | Volume 3 (14–18) |
| Persistence | SQLite, zero data loss across swaps | ch. 32 + ch. 28 |
| Pagination & filtering | ?page=&size=&done= | ch. 18 |
| Token auth | X-Api-Token header | ch. 27/39 |
| Ops surface | /health, /stats, /admin/reload | ch. 26–27/38 |
| Deployment shape | single directory + systemd | ch. 42 |
45.2 The foundation (30 minutes)
- Skeleton: lay out directories per ch. 05's conventions (modules/ route_http/ db/ wwwroot/); main.c orchestrates includes; xs.json gets the minimal config per ch. 07/08
- Three modules: modules/http.h (ch. 15's Reply family), modules/db.h (ch. 32's DB_Init/Unit), modules/route.h (ch. 17's two tables)
- Table:
todo(id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, done INTEGER DEFAULT 0, created INTEGER)
Acceptance 1: the service starts, all five log segments present (ch. 02's dictionary), /health returns ok.
45.3 CRUD & conventions
Create and update (the two core pieces)
/* POST /api/v1/todos: read JSON (ch. 16+18) → bound insert (ch. 32) */
static void Handle_Create(XS_HttpReq* pReq, const RouteParamHTTP* a, uint32 n)
{
xvalue* pBody = ReadRequestJSON(pReq); /* wrapper: body→parse */
if ( pBody == NULL ) return (void)ApiError(pReq, 400, "bad_json");
xvalue* pTitle = xrtValueObjectGet(pBody, "title");
if ( pTitle == NULL || !IsNonEmptyString(pTitle) )
return (void)(xrtValueRelease(pBody),
ApiError(pReq, 422, "field_required:title"));
...
int64 iId = DB_InsertTodo(ViewOf(pTitle));
xrtValueRelease(pBody);
ReplyItemJSON(pReq, 201, iId); /* 201 + the new resource */
}
/* PUT/PATCH /api/v1/todos/{id}: path params + partial update */
/* the three-way error contract: 404 (absent) / 422 (field) / 200 (ok) */
Error contract first (the key habit in API work): 400 malformed JSON, 401 token, 404 resource, 405 method (routing automatic), 413 oversized (driver automatic), 422 semantic — each with a machine-readable code field. PUT on an absent resource: 404 or upsert? Pick one and document it; never both.
Acceptance 2: all seven CRUD endpoints pass curl; the three-way error contract behaves correctly under three deliberate inputs.
45.4 Pagination & auth
- Pagination: the list endpoint
SELECT ... LIMIT ? OFFSET ?+ a totalSELECT COUNT(*); response{"total":N,"page":P,"data":[...]}— an upper clamp (e.g. size ≤ 100) is a mandatory defense - Auth middleware: one module-layer
CheckToken(pReq)(constant-time comparison), called as the first line of every /api/* handler; the token lives in a Custom knob (ch. 09's strict reading) - Hot-reload drill: change the list ordering rule → reload → verify (a) zero in-flight errors (b) data intact (ch. 32 generations) (c) counters continuous across swaps (Swap carries a total counter — ch. 28's graduation check)
Acceptance 3: three swaps under load with zero failures; the total counter continuous; wrong tokens 401 with rate limiting active.
45.5 The acceptance checklist
| Dimension | Standard | Chapter basis |
|---|---|---|
| Function | CRUD/pagination/filtering/auth all pass | this chapter |
| Protocol | HEAD supported (GET|HEAD registration), 405+Allow correct | ch. 15/17 |
| Persistence | zero loss across restarts and swaps | ch. 32 |
| Resilience | zero failures swapping under load; all four limit states correct | ch. 28/09 |
| Observability | /health /stats usable; request logs carry IDs | ch. 43 |
| Deployment | systemd unit + a graceful-shutdown window | ch. 42 |
Common mistakes
Writing endpoints before fixing the error contract
- Symptom
- Unifying the error format late means touching every handler.
- Cause
- The error contract is the API's constitution — fix it in 45.3 step one (the ApiError helper + a code table).
No clamp on the size parameter
- Symptom
- One ?size=1000000 and the service starts deep-pagination scans.
- Cause
- Pagination parameters are client input — an upper clamp and a default are part of the interface.
Exercises
BasicComplete the project
Pass every acceptance point from 45.2–45.5.
Acceptance: all six checklist dimensions green.
AdvancedConcurrent-edit semantics
Add an updated field with If-Match optimistic locking (409 on conflict).
Acceptance: of two concurrent PUTs exactly one succeeds.
ChallengeAn OpenAPI documentation route
Implement /api/openapi.json: describe the endpoints with xvalue (route-table-driven docs).
Acceptance: zero drift between docs and implementation (same-source generation).
Summary
| Point | Conclusion |
|---|---|
| Flow | skeleton → three modules → error contract → CRUD → pagination/auth → acceptance |
| Key habits | error contract first; parameter clamps; a unified auth entry |
| Graduation mark | zero failures swapping under load + data intact + counters continuous |
API references in this chapter
xrt further reading
- xrt · value and json — this project's main tools