API Integration
REST API integration: lessons from the frontend side
The frontend is where an API's design decisions come home to roost. A clean, consistent API is a joy to consume; an inconsistent one leaks its quirks into every component that touches it. Since the frontend rarely gets to choose the API, the real skill is building an integration layer that stays sane regardless.
These lessons come from years of wiring React apps to REST backends — some beautifully designed, some legacy, some mid-rewrite with two naming conventions living side by side. The patterns below are the ones that kept working when the APIs didn't.
Model the request, not just the response
Every fetch has four states — idle, loading, success, error — and the UI has to have an answer for all of them. Treating those states as first-class from the start is what keeps a screen from flashing empty, hanging on a spinner forever, or crashing on the response shape nobody tested.
In practice I reach for React Query-style data hooks rather than hand-rolled useEffect fetching, because the hard parts — caching, deduplication, refetching, race conditions — are exactly the parts that hand-rolled versions get subtly wrong. The component asks for data and renders four states; everything else is the data layer's problem.
Isolate the boundary
Components never talk to fetch directly. All API calls live behind a thin data layer — typed functions per endpoint — and that boundary is where I normalise response shapes, map backend naming to frontend naming, handle auth headers, and swap base URLs per environment. The rest of the app doesn't know where data comes from, which means the rest of the app doesn't break when the answer changes.
This is also where backend inconsistency gets quarantined. When one endpoint returns `user_name` and another returns `userName`, the mapping happens once, at the boundary, and the entire application speaks one dialect. Without that discipline, every quirk of the API becomes a permanent resident of your component tree.
Type the contract, then verify it
TypeScript types for API responses are only as truthful as their source. Generated types from an OpenAPI spec are best; runtime validation with something like Zod at the boundary is the next best, because it converts 'the API changed and the UI broke somewhere weird' into 'the API changed and here is the exact field that no longer matches'.
The failure mode worth engineering against is the silent one: an endpoint starts returning null in a field that was always populated, and three screens away a `toFixed()` call throws. Validation at the boundary turns distant, mysterious crashes into loud, local, debuggable errors — trading a production incident for a log line.
Design the error experience like a feature
Errors aren't an edge case; they're weather. Networks drop, tokens expire, servers deploy. The integration layer should classify failures — network, auth, validation, server — because each deserves different UI: a retry button for network blips, a re-login flow for expired sessions, field-level messages for validation, and an honest apology for 500s.
Two details that separate polished apps from fragile ones: retry with backoff for idempotent GETs (transient failures should heal invisibly), and optimistic updates with rollback for the interactions where waiting feels broken — likes, toggles, reorders. Both live in the data layer, invisible to components, which is exactly where that complexity belongs.
Loading is a design problem, not a technical one
How an app waits is part of how it feels. Skeletons that match the eventual layout beat spinners because nothing jumps when data lands. Stale-while-revalidate beats blank-then-load because users see something immediately. Pagination and infinite scroll are API integration decisions that masquerade as UX decisions — the endpoint's shape decides what the scroll experience can be.
The principle underneath: never make the user pay for the network twice. Cache what was already fetched, prefetch what they'll probably want next, and reserve full-screen loading states for the first visit only. An integration layer that handles this well makes an average API feel fast; one that doesn't makes a fast API feel broken.
