feat: add frontend API integration patterns skill

Add frontend API integration patterns with maintainer hardening for retries, cancellation, metadata, and limitations.
This commit is contained in:
Avinash jain
2026-04-24 05:47:36 +05:30
committed by GitHub
parent 9dba3b844a
commit 22eba23ca7

View File

@@ -0,0 +1,342 @@
---
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
---