mirror of
https://github.com/sickn33/antigravity-awesome-skills.git
synced 2026-04-25 17:25:12 +02:00
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:
342
skills/frontend-api-integration-patterns/SKILL.md
Normal file
342
skills/frontend-api-integration-patterns/SKILL.md
Normal 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
|
||||
|
||||
---
|
||||
Reference in New Issue
Block a user