API client — typed fetch, query params, and errors that throw

@anshinus/api

Exercised over real HTTP against this app's own static assets — a 200 from a fixture and a genuine 404 from the file server. fetch() resolves on an error status; this client does not, which is the whole point. The arbitrary-status route handler this page used to call did not survive the move to a static export; the cards below say exactly what was lost.

buildQueryParams

Handles the cases hand-built strings get wrong: arrays become repeated keys, undefined is dropped rather than serialised as the string “undefined”, false is kept (it is a value, not an absence), and everything is encoded.

{
 "status": "Approved",
 "tags": [
  "hipaa",
  "phi"
 ],
 "page": 2,
 "includeArchived": false,
 "owner": "«undefined»",
 "note": "a value with spaces & an ampersand"
}

becomes

status=Approved&tags=hipaa&tags=phi&page=2&includeArchived=false&note=a+value+with+spaces+%26+an+ampersand

Note owner is absent entirely and includeArchived=false is present. Dropping a false is a common hand-rolled bug — it flips the server's default.

apiFetch — a real round trip

GETs /api/echo.json, a static fixture served over real HTTP. Headers, parsing and response typing are the client's job and are genuinely exercised; the generic parameter types the response. It was a POST to a route handler that echoed the request body until this demo moved to a static export — a static file cannot vary by request, so the reflection is gone and the round trip is not.

Errors — the part worth demonstrating

fetch() does not reject on 4xx or 5xx — it resolves with ok: false. Code written as const data = await res.json() then happily uses an error body as data. apiFetch throws a typed ApiError instead, so a failure cannot be mistaken for a result.

→ a real 404 from the file server

This offered 400, 401, 403, 404, 500 and 503 until the demo moved to a static export. A static file server cannot be asked to return a chosen status, so the other five were removed rather than simulated — a faked status would demonstrate the mock, not the client. The 404 body is HTML or empty rather than JSON for the same reason, so ApiError.body carrying a server's JSON error is described below instead of shown here.

Brand