343 lines
7.5 KiB
Markdown
343 lines
7.5 KiB
Markdown
---
|
|
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
|
|
|
|
---
|