One Binary, Two Worlds
Talking to the API
Lesson 3 of 5
What you'll learn
- Write one small typed
api<T>()helper instead of scattering rawfetchcalls - Model server data as three states: loading, error, data
- Apply an optimistic update, and roll it back when the request fails
Raw fetch has two traps: it only rejects on network failure — a 500 resolves happily — and it hands back untyped any. Wrap it once, use it everywhere:
// web/src/api.ts
export async function api<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(path, {
headers: { "Content-Type": "application/json" },
...init,
});
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
return res.json() as Promise<T>;
}
export type Note = { id: string; title: string; createdAt: string };
// api<Note[]>("/api/notes") — typed list
// api<Note>("/api/notes", { method: "POST", body: JSON.stringify({ title }) })
The T mirrors the JSON contract from lesson 1 — the Go structs and these types describe the same wire shapes. One helper also gives you one place to add auth handling later (lesson 4).
Three states, no lying
Server data is never just "the data" — it's loading, error, or data, and the UI should render all three honestly. The bare-hooks version: useState for each, an effect that fetches, early returns for the first two states. Libraries like TanStack Query package this pattern; knowing the bare version tells you what they're doing.
Optimistic updates
For mutations, waiting for the round trip makes the UI feel sluggish. The optimistic pattern: apply the change to local state immediately, fire the request, and if it fails, roll back to a snapshot and surface the error. The user sees instant success in the common case and the truth in the rare one.
Snapshot before you touch state
The rollback is only as good as its snapshot. Capture the previous list before applying the optimistic change, and restore exactly that on failure — recomputing "what it probably was" is how ghosts and duplicates creep in.
This challenge is real React, not a model: a component over a faked API where any title containing "fail" is rejected by the server — watch the optimistic note appear, then vanish on rollback.
Add a note — it appears instantly, then its pending mark clears when the server confirms. Now add one with 'fail' in the title: it appears, the fake server rejects it, and the list rolls back to the snapshot.
Why does the api<T>() helper throw when res.ok is false?
Next: the boundary gets a bouncer — session cookies vs bearer tokens, Go auth middleware around ServeMux routes, and what the SPA does with a 401.
Saved on this device. Sign in to sync your progress everywhere.