--- name: frontend-api-integration-patterns description: "Production-ready patterns for integrating frontend applications with backend APIs, including race condition handling, request cancellation, retry strategies, error normalization, and UI state management." category: frontend risk: safe source: community date_added: "2026-04-23" author: avij1109 tags: - frontend - api-integration - javascript - react - async tools: - claude - cursor - gemini - codex --- # Frontend API Integration Patterns ## Overview This skill provides production-ready patterns for integrating frontend applications with backend APIs. Most frontend issues are not caused by APIs being difficult to call, but by **incorrect handling of asynchronous behavior**—leading to race conditions, stale data, duplicated requests, and poor user experience. This skill focuses on **correctness, resilience, and user experience**, not just making API calls work. --- ## When to Use This Skill * Connecting frontend apps (React, React Native, Vue, etc.) to backend APIs * Integrating ML/AI endpoints (`/predict`, `/recommend`) * Handling asynchronous data in UI * Fixing stale data, flickering UI, or duplicate requests * Designing scalable frontend API layers --- ## Core Patterns ### 1. API Layer (Separation of Concerns) Centralize API logic and normalize errors. ```js id="k1m7r2" export class ApiError extends Error { constructor(message, status, payload = null) { super(message); this.name = "ApiError"; this.status = status; this.payload = payload; } } export const apiClient = async (url, options = {}) => { const res = await fetch(url, { headers: { "Content-Type": "application/json" }, ...options, }); if (!res.ok) { let payload = null; try { payload = await res.json(); } catch (_) {} throw new ApiError( payload?.message || "Request failed", res.status, payload ); } // handle empty responses safely (e.g. 204 No Content) if (res.status === 204) return null; const text = await res.text(); return text ? JSON.parse(text) : null; }; ``` --- ### 2. Race-Safe State Management Prevent stale responses from overwriting fresh data. ```js id="y7p4ha" useEffect(() => { let cancelled = false; const load = async () => { try { setLoading(true); setError(null); const result = await getUser(); if (!cancelled) setData(result); } catch (err) { if (!cancelled) setError(err.message); } finally { if (!cancelled) setLoading(false); } }; load(); return () => { cancelled = true; }; }, []); ``` > Use a cancellation flag for non-fetch async logic. For network requests, prefer AbortController. --- ### 3. Request Cancellation (AbortController) Cancel in-flight requests to avoid memory leaks and stale updates. ```js id="l9x2pw" useEffect(() => { const controller = new AbortController(); const load = async () => { try { const data = await getUser({ signal: controller.signal }); setData(data); } catch (err) { if (err.name === "AbortError") return; setError(err.message); } }; load(); return () => controller.abort(); }, [userId]); ``` --- ### 4. Retry with Exponential Backoff Retry only transient failures (5xx or network errors). ```js id="8n3zcf" const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const fetchWithBackoff = async (fn, retries = 3, delay = 300) => { try { return await fn(); } catch (err) { const isAbort = err.name === "AbortError"; const isHttpError = typeof err.status === "number"; const isRetryable = !isAbort && (!isHttpError || err.status >= 500); if (retries <= 0 || !isRetryable) throw err; const nextDelay = delay * 2 + Math.random() * 100; await sleep(nextDelay); return fetchWithBackoff(fn, retries - 1, nextDelay); } }; ``` --- ### 5. Debounced API Calls Avoid excessive API calls (e.g., search inputs). ```js id="i2r7wq" const useDebounce = (value, delay = 400) => { const [debounced, setDebounced] = useState(value); useEffect(() => { const t = setTimeout(() => setDebounced(value), delay); return () => clearTimeout(t); }, [value, delay]); return debounced; }; ``` --- ### 6. Request Deduplication Prevent duplicate API calls across components. ```js id="x8v4km" const inFlight = new Map(); export const dedupedFetch = (key, fn) => { if (inFlight.has(key)) return inFlight.get(key); const promise = fn().finally(() => inFlight.delete(key)); inFlight.set(key, promise); return promise; }; ``` --- ## Examples ### Example 1: ML Prediction with Cancellation ```js id="n5q2pt" const controllerRef = useRef(null); const handlePredict = async (input) => { controllerRef.current?.abort(); controllerRef.current = new AbortController(); try { const result = await fetchWithBackoff(() => apiClient("/predict", { method: "POST", body: JSON.stringify({ text: input }), signal: controllerRef.current.signal, }) ); setOutput(result); } catch (err) { if (err.name === "AbortError") return; setError(err.message); } }; ``` --- ### Example 2: Debounced Search ```js id="w4z8yn" const debouncedQuery = useDebounce(query, 400); useEffect(() => { if (!debouncedQuery) return; const controller = new AbortController(); searchAPI(debouncedQuery, { signal: controller.signal }) .then(setResults) .catch((err) => { if (err.name !== "AbortError") { setError("Search failed. Please try again."); } }); return () => controller.abort(); }, [debouncedQuery]); ``` --- ### Example 3: Optimistic UI Update ```js id="q2k9hz" const deleteItem = async (id) => { const previous = items; setItems((curr) => curr.filter((item) => item.id !== id)); try { await apiClient(`/items/${id}`, { method: "DELETE" }); } catch (err) { setItems(previous); setError("Delete failed. Please try again."); } }; ``` --- ## Best Practices * ✅ Centralize API logic in a dedicated layer * ✅ Normalize errors using a custom error class * ✅ Always handle loading, error, and success states * ✅ Use AbortController for request cancellation * ✅ Retry only transient failures (5xx) * ✅ Use debouncing for input-driven APIs * ✅ Deduplicate identical requests --- ## Anti-Patterns * ❌ Retrying 4xx errors * ❌ No request cancellation (memory leaks) * ❌ Race-condition-prone state updates * ❌ Swallowing errors silently * ❌ Global loading/error state for multiple requests * ❌ Calling APIs directly inside components repeatedly --- ## Common Pitfalls **Problem:** UI shows stale data **Solution:** Use cancellation or guard against outdated responses **Problem:** Too many API calls on input **Solution:** Use debouncing + cancellation **Problem:** Duplicate requests from multiple components **Solution:** Use request deduplication **Problem:** Server overload during retry **Solution:** Use exponential backoff **Problem:** State updates after component unmount **Solution:** Use AbortController cleanup --- ## Limitations * These examples use vanilla JavaScript patterns; adapt them to your framework's data-fetching library when using React Query, SWR, Apollo, Relay, or similar tools. * Do not retry non-idempotent mutations unless the backend provides idempotency keys or another duplicate-safe contract. * Do not expose privileged API keys in frontend code; proxy sensitive requests through a backend. --- ## Additional Resources * https://developer.mozilla.org/en-US/docs/Web/API/AbortController * https://react.dev * https://axios-http.com ---