API Integration Modern Patterns
1. RESTful Services Axios Fetch API
| Tool/Library | Features | Use Case | Advantages |
|---|---|---|---|
| Fetch API | Native browser API, Promise-based | Modern browsers, simple requests, no dependencies | Built-in, streaming support, standards-compliant |
| Axios | Interceptors, auto JSON transform, timeout | Complex apps, request/response manipulation, older browsers | Request/response interceptors, automatic retries, progress tracking |
| TanStack Query (React Query) | Caching, auto refetch, optimistic updates | Data fetching with caching, background sync, pagination | Built-in cache, loading states, automatic refetching |
| SWR | Stale-while-revalidate, real-time updates | Real-time dashboards, fast UI updates, simple API | Lightweight, automatic revalidation, focus tracking |
| RTK Query | Redux integration, code generation | Redux apps, type-safe APIs, normalized cache | Redux DevTools, normalized cache, tag-based invalidation |
Example: RESTful API Integration
// Fetch API - Native
async function fetchUsers() {
try {
const response = await fetch('https://api.example.com/users', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Fetch error:', error);
throw error;
}
}
// POST request with Fetch
async function createUser(userData) {
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(userData)
});
return response.json();
}
// Axios - Configure instance
import axios from 'axios';
const api = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
});
// Request interceptor
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor
api.interceptors.response.use(
(response) => response.data,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const { token } = await refreshToken();
localStorage.setItem('token', token);
originalRequest.headers.Authorization = `Bearer ${token}`;
return api(originalRequest);
} catch (refreshError) {
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
// Use Axios
async function getUsers() {
try {
const users = await api.get('/users');
return users;
} catch (error) {
console.error('Error fetching users:', error);
}
}
async function updateUser(id, data) {
return api.patch(`/users/${id}`, data);
}
// TanStack Query (React Query)
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function UsersList() {
const queryClient = useQueryClient();
// Fetch users
const { data: users, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: () => api.get('/users'),
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000
});
// Create user mutation
const createMutation = useMutation({
mutationFn: (newUser) => api.post('/users', newUser),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
}
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
{users.map(user => <UserCard key={user.id} user={user} />)}
<button onClick={() => createMutation.mutate({ name: 'New User' })}>
Add User
</button>
</div>
);
}
// SWR - Simple data fetching
import useSWR from 'swr';
const fetcher = (url) => fetch(url).then(res => res.json());
function Dashboard() {
const { data, error, isLoading } = useSWR('/api/dashboard', fetcher, {
refreshInterval: 3000, // Refresh every 3 seconds
revalidateOnFocus: true
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Failed to load</div>;
return <div>{data.stats}</div>;
}
// SWR with mutation
import useSWRMutation from 'swr/mutation';
async function updateUser(url, { arg }) {
return fetch(url, {
method: 'PATCH',
body: JSON.stringify(arg)
}).then(res => res.json());
}
function UserProfile({ userId }) {
const { data } = useSWR(`/api/users/${userId}`, fetcher);
const { trigger, isMutating } = useSWRMutation(`/api/users/${userId}`, updateUser);
return (
<div>
<h1>{data?.name}</h1>
<button onClick={() => trigger({ name: 'Updated Name' })} disabled={isMutating}>
Update
</button>
</div>
);
}
Modern Approach: Use TanStack Query or SWR for data fetching.
They handle caching, loading states, and refetching automatically, reducing boilerplate by 70%.
2. GraphQL Apollo Client Relay
| Client Library | Features | Complexity | Use Case |
|---|---|---|---|
| Apollo Client | Normalized cache, DevTools, subscriptions | Medium | Most popular, production-ready, great DX |
| Relay | Compile-time optimization, Facebook-backed | High | Large-scale apps, maximum performance, strict patterns |
| urql | Lightweight, extensible, customizable cache | Low | Smaller bundle, flexible caching, simpler setup |
| graphql-request | Minimal wrapper over fetch | Very Low | Simple queries, no caching needed, lightweight |
Example: GraphQL Integration
// Apollo Client Setup
import { ApolloClient, InMemoryCache, ApolloProvider, gql } from '@apollo/client';
const client = new ApolloClient({
uri: 'https://api.example.com/graphql',
cache: new InMemoryCache(),
headers: {
authorization: `Bearer ${token}`
}
});
function App() {
return (
<ApolloProvider client={client}>
<Users />
</ApolloProvider>
);
}
// Query with Apollo
import { useQuery, useMutation } from '@apollo/client';
const GET_USERS = gql`
query GetUsers($limit: Int!) {
users(limit: $limit) {
id
name
email
posts {
id
title
}
}
}
`;
function Users() {
const { loading, error, data, refetch } = useQuery(GET_USERS, {
variables: { limit: 10 },
fetchPolicy: 'cache-first' // cache-first, network-only, cache-and-network
});
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
{data.users.map(user => (
<div key={user.id}>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
))}
<button onClick={() => refetch()}>Refresh</button>
</div>
);
}
// Mutation with Apollo
const CREATE_USER = gql`
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
`;
function CreateUserForm() {
const [createUser, { loading, error }] = useMutation(CREATE_USER, {
refetchQueries: [{ query: GET_USERS, variables: { limit: 10 } }],
// or update cache manually
update(cache, { data: { createUser } }) {
cache.modify({
fields: {
users(existingUsers = []) {
const newUserRef = cache.writeFragment({
data: createUser,
fragment: gql`
fragment NewUser on User {
id
name
email
}
`
});
return [...existingUsers, newUserRef];
}
}
});
}
});
const handleSubmit = (e) => {
e.preventDefault();
createUser({ variables: { input: { name: 'John', email: 'john@example.com' } } });
};
return <form onSubmit={handleSubmit}>{/* form fields */}</form>;
}
// Apollo Subscriptions (WebSocket)
import { useSubscription } from '@apollo/client';
const MESSAGE_SUBSCRIPTION = gql`
subscription OnMessageAdded {
messageAdded {
id
content
user {
name
}
}
}
`;
function Chat() {
const { data, loading } = useSubscription(MESSAGE_SUBSCRIPTION);
return loading ? <p>Loading...</p> : <Message message={data.messageAdded} />;
}
// urql - Lightweight alternative
import { createClient, Provider, useQuery } from 'urql';
const client = createClient({
url: 'https://api.example.com/graphql'
});
function App() {
return (
<Provider value={client}>
<Users />
</Provider>
);
}
function Users() {
const [result] = useQuery({
query: GET_USERS,
variables: { limit: 10 }
});
const { data, fetching, error } = result;
if (fetching) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return <div>{/* render users */}</div>;
}
// graphql-request - Minimal
import { request, gql } from 'graphql-request';
const endpoint = 'https://api.example.com/graphql';
const query = gql`
query GetUsers {
users {
id
name
}
}
`;
async function fetchUsers() {
const data = await request(endpoint, query);
console.log(data.users);
}
// Code Generation with GraphQL Code Generator
// codegen.yml
schema: https://api.example.com/graphql
documents: './src/**/*.graphql'
generates:
./src/generated/graphql.tsx:
plugins:
- typescript
- typescript-operations
- typescript-react-apollo
// Generated types and hooks
import { useGetUsersQuery } from './generated/graphql';
function Users() {
const { data, loading } = useGetUsersQuery({ variables: { limit: 10 } });
// Fully typed!
}
When to Use GraphQL: Multiple related resources, mobile apps (reduce requests), avoid over-fetching. REST is simpler for CRUD APIs with fixed data shapes.
3. tRPC Type-safe API Calls
| Feature | Description | Benefit |
|---|---|---|
| End-to-end Type Safety | TypeScript types from backend to frontend | Zero code generation, compile-time safety, autocomplete |
| No Schema Definition | No GraphQL schema or OpenAPI spec needed | Less boilerplate, faster development, DX excellence |
| React Query Integration | Built on TanStack Query | Automatic caching, loading states, optimistic updates |
| Lightweight | Minimal bundle size, simple API | Fast, no code generation step, instant feedback |
| Zod Validation | Runtime validation with Zod schemas | Type-safe input validation, automatic error handling |
Example: tRPC Implementation
// Backend - tRPC Router (Next.js API route or Express)
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.create();
const appRouter = t.router({
// Query (GET)
getUsers: t.procedure
.input(z.object({ limit: z.number().optional() }))
.query(async ({ input }) => {
const users = await db.user.findMany({
take: input.limit || 10
});
return users;
}),
// Query with ID
getUserById: t.procedure
.input(z.string())
.query(async ({ input }) => {
const user = await db.user.findUnique({ where: { id: input } });
if (!user) throw new Error('User not found');
return user;
}),
// Mutation (POST/PUT/DELETE)
createUser: t.procedure
.input(z.object({
name: z.string().min(3),
email: z.string().email(),
age: z.number().min(18).optional()
}))
.mutation(async ({ input }) => {
const user = await db.user.create({ data: input });
return user;
}),
updateUser: t.procedure
.input(z.object({
id: z.string(),
name: z.string().optional(),
email: z.string().email().optional()
}))
.mutation(async ({ input }) => {
const { id, ...data } = input;
return db.user.update({ where: { id }, data });
}),
deleteUser: t.procedure
.input(z.string())
.mutation(async ({ input }) => {
await db.user.delete({ where: { id: input } });
return { success: true };
})
});
export type AppRouter = typeof appRouter;
// Next.js API route - pages/api/trpc/[trpc].ts
import { createNextApiHandler } from '@trpc/server/adapters/next';
export default createNextApiHandler({
router: appRouter,
createContext: () => ({})
});
// Frontend - tRPC Client Setup
import { createTRPCReact } from '@trpc/react-query';
import { httpBatchLink } from '@trpc/client';
import type { AppRouter } from './server/router';
export const trpc = createTRPCReact<AppRouter>();
// _app.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useState } from 'react';
function App({ Component, pageProps }) {
const [queryClient] = useState(() => new QueryClient());
const [trpcClient] = useState(() =>
trpc.createClient({
links: [
httpBatchLink({
url: 'http://localhost:3000/api/trpc',
headers() {
return {
authorization: getAuthToken()
};
}
})
]
})
);
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
</QueryClientProvider>
</trpc.Provider>
);
}
// Using tRPC in Components
function UsersList() {
// Query - fully typed!
const { data: users, isLoading } = trpc.getUsers.useQuery({ limit: 20 });
// Mutation
const utils = trpc.useContext();
const createMutation = trpc.createUser.useMutation({
onSuccess: () => {
utils.getUsers.invalidate(); // Refetch users
}
});
const handleCreate = () => {
createMutation.mutate({
name: 'John Doe',
email: 'john@example.com',
age: 25
});
};
if (isLoading) return <div>Loading...</div>;
return (
<div>
{users?.map(user => (
<UserCard key={user.id} user={user} />
))}
<button onClick={handleCreate}>Create User</button>
</div>
);
}
// Single user query
function UserProfile({ userId }: { userId: string }) {
const { data: user } = trpc.getUserById.useQuery(userId);
const updateMutation = trpc.updateUser.useMutation();
return (
<div>
<h1>{user?.name}</h1>
<button onClick={() => updateMutation.mutate({ id: userId, name: 'Updated' })}>
Update
</button>
</div>
);
}
// Optimistic Updates
const deleteMutation = trpc.deleteUser.useMutation({
onMutate: async (deletedId) => {
await utils.getUsers.cancel();
const previousUsers = utils.getUsers.getData();
utils.getUsers.setData(undefined, (old) =>
old?.filter(user => user.id !== deletedId)
);
return { previousUsers };
},
onError: (err, deletedId, context) => {
utils.getUsers.setData(undefined, context?.previousUsers);
},
onSettled: () => {
utils.getUsers.invalidate();
}
});
// Middleware (Authentication)
const isAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.user) {
throw new Error('Unauthorized');
}
return next({ ctx: { user: ctx.user } });
});
const protectedProcedure = t.procedure.use(isAuthed);
const protectedRouter = t.router({
getProfile: protectedProcedure.query(({ ctx }) => {
return ctx.user; // user is typed!
})
});
tRPC is Perfect For: Full-stack TypeScript apps where you control both frontend and backend.
Get 100% type safety with zero code generation.
4. Offline-First IndexedDB Strategies
| Storage API | Capacity | API Type | Use Case |
|---|---|---|---|
| IndexedDB | ~50% of disk space | Async, transactional | Large datasets, complex queries, offline apps |
| localStorage | 5-10 MB | Synchronous, key-value | Small data, user preferences, simple storage |
| Cache API | Varies by browser | Async, HTTP responses | Service Worker caching, PWA offline support |
| Web SQL DEPRECATED | ~50 MB | SQL database | Legacy only, use IndexedDB instead |
Example: Offline-First with IndexedDB
// Dexie.js - IndexedDB wrapper
import Dexie from 'dexie';
const db = new Dexie('MyAppDatabase');
db.version(1).stores({
users: '++id, name, email, updatedAt',
posts: '++id, userId, title, content, createdAt'
});
// Add data
async function addUser(user) {
const id = await db.users.add({
...user,
updatedAt: Date.now()
});
return id;
}
// Query data
async function getUsers() {
return db.users.toArray();
}
async function getUserById(id) {
return db.users.get(id);
}
async function searchUsers(query) {
return db.users
.filter(user => user.name.toLowerCase().includes(query.toLowerCase()))
.toArray();
}
// Update data
async function updateUser(id, changes) {
return db.users.update(id, {
...changes,
updatedAt: Date.now()
});
}
// Delete data
async function deleteUser(id) {
return db.users.delete(id);
}
// React Hook for IndexedDB
import { useLiveQuery } from 'dexie-react-hooks';
function UsersList() {
const users = useLiveQuery(() => db.users.toArray(), []);
if (!users) return <div>Loading...</div>;
return (
<div>
{users.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
}
// Offline-First Sync Strategy
class OfflineFirstSync {
constructor(apiUrl) {
this.apiUrl = apiUrl;
this.syncQueue = [];
}
// Fetch with offline fallback
async fetchWithCache(endpoint) {
try {
// Try network first
const response = await fetch(`${this.apiUrl}${endpoint}`);
const data = await response.json();
// Store in IndexedDB
await db.users.bulkPut(data);
return data;
} catch (error) {
// Network failed, use cached data
console.log('Using cached data');
return db.users.toArray();
}
}
// Queue operations when offline
async queueOperation(operation) {
this.syncQueue.push({
id: Date.now(),
operation,
timestamp: Date.now()
});
// Save queue to IndexedDB
await db.syncQueue.add(this.syncQueue[this.syncQueue.length - 1]);
// Try to sync immediately
this.syncQueuedOperations();
}
// Sync queued operations when online
async syncQueuedOperations() {
if (!navigator.onLine) return;
const pending = await db.syncQueue.toArray();
for (const item of pending) {
try {
await this.executeOperation(item.operation);
await db.syncQueue.delete(item.id);
} catch (error) {
console.error('Sync failed:', error);
break; // Stop on first error
}
}
}
async executeOperation(operation) {
const { method, endpoint, data } = operation;
return fetch(`${this.apiUrl}${endpoint}`, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
}
}
// Online/Offline Detection
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}
function App() {
const isOnline = useOnlineStatus();
return (
<div>
{!isOnline && <div className="offline-banner">You are offline</div>}
{/* app content */}
</div>
);
}
// PouchDB - CouchDB sync
import PouchDB from 'pouchdb';
const localDB = new PouchDB('myapp');
const remoteDB = new PouchDB('https://mycouch.example.com/myapp');
// Sync databases
localDB.sync(remoteDB, {
live: true,
retry: true
}).on('change', (info) => {
console.log('Sync change:', info);
}).on('error', (err) => {
console.error('Sync error:', err);
});
// Add document
await localDB.put({
_id: 'user-1',
name: 'John Doe',
email: 'john@example.com'
});
// Query document
const user = await localDB.get('user-1');
// Find documents
const result = await localDB.find({
selector: { name: { $regex: /John/ } }
});
Offline-First Strategy: Local first, sync later. Store all data
locally, sync with server when online. Provides instant UI updates and works offline.
5. API Mocking MSW Mock Service Worker
| Tool | Approach | Use Case | Advantage |
|---|---|---|---|
| MSW (Mock Service Worker) | Intercept network at service worker level | Development, testing, demos without backend | Works in browser and Node.js, realistic network behavior |
| JSON Server | Full REST API from JSON file | Rapid prototyping, quick backend mock | Zero config, CRUD operations, relationships |
| MirageJS | In-memory API mock server | Frontend development, complex scenarios | Database simulation, relationships, factories |
| Nock | HTTP mocking for Node.js | Backend tests, integration tests | Record/replay, precise request matching |
Example: API Mocking with MSW
// MSW Setup - src/mocks/handlers.js
import { http, HttpResponse } from 'msw';
export const handlers = [
// GET request
http.get('/api/users', () => {
return HttpResponse.json([
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' }
]);
}),
// GET with params
http.get('/api/users/:id', ({ params }) => {
const { id } = params;
return HttpResponse.json({
id,
name: 'John Doe',
email: 'john@example.com'
});
}),
// POST request
http.post('/api/users', async ({ request }) => {
const body = await request.json();
return HttpResponse.json(
{ id: 3, ...body },
{ status: 201 }
);
}),
// Error response
http.get('/api/error', () => {
return HttpResponse.json(
{ message: 'Internal server error' },
{ status: 500 }
);
}),
// Delayed response
http.get('/api/slow', async () => {
await new Promise(resolve => setTimeout(resolve, 2000));
return HttpResponse.json({ data: 'Slow response' });
}),
// GraphQL mock
http.post('/graphql', async ({ request }) => {
const { query } = await request.json();
if (query.includes('GetUsers')) {
return HttpResponse.json({
data: {
users: [
{ id: '1', name: 'John' },
{ id: '2', name: 'Jane' }
]
}
});
}
})
];
// Browser setup - src/mocks/browser.js
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';
export const worker = setupWorker(...handlers);
// Start in development
// src/index.tsx
import { worker } from './mocks/browser';
if (process.env.NODE_ENV === 'development') {
worker.start();
}
// Node.js setup (for tests) - src/mocks/server.js
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
// Test setup - setupTests.ts
import { server } from './mocks/server';
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
// Override handler in test
import { http, HttpResponse } from 'msw';
import { server } from './mocks/server';
test('handles server error', async () => {
server.use(
http.get('/api/users', () => {
return HttpResponse.json(
{ message: 'Server error' },
{ status: 500 }
);
})
);
// Test error handling
});
// JSON Server - db.json
{
"users": [
{ "id": 1, "name": "John Doe", "email": "john@example.com" },
{ "id": 2, "name": "Jane Smith", "email": "jane@example.com" }
],
"posts": [
{ "id": 1, "userId": 1, "title": "First Post", "content": "..." }
]
}
// Start JSON Server
npx json-server --watch db.json --port 3001
// API endpoints auto-generated:
// GET /users
// GET /users/1
// POST /users
// PUT /users/1
// PATCH /users/1
// DELETE /users/1
// GET /posts?userId=1
// MirageJS Setup
import { createServer, Model } from 'miragejs';
createServer({
models: {
user: Model
},
seeds(server) {
server.create('user', { name: 'John', email: 'john@example.com' });
server.create('user', { name: 'Jane', email: 'jane@example.com' });
},
routes() {
this.namespace = 'api';
this.get('/users', (schema) => {
return schema.users.all();
});
this.get('/users/:id', (schema, request) => {
const id = request.params.id;
return schema.users.find(id);
});
this.post('/users', (schema, request) => {
const attrs = JSON.parse(request.requestBody);
return schema.users.create(attrs);
});
}
});
MSW Recommendation: Use MSW for both development and testing.
Single source of truth for mocks, works seamlessly across environments.
6. Rate Limiting Retry Logic Implementation
| Strategy | Implementation | Use Case | Backoff Formula |
|---|---|---|---|
| Exponential Backoff | Increase delay exponentially between retries | API rate limits, transient errors | delay = baseDelay * 2^attempt |
| Linear Backoff | Fixed delay increase between retries | Simple retry scenarios | delay = baseDelay * attempt |
| Exponential + Jitter | Add randomness to exponential backoff | Prevent thundering herd, distributed systems | delay = random(0, 2^attempt * base) |
| Circuit Breaker | Stop requests after threshold failures | Failing services, prevent cascade failures | Open → Half-Open → Closed states |
Example: Retry Logic and Rate Limiting
// Exponential Backoff with Retry
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
// Retry on 5xx errors or rate limit
if (response.status >= 500 || response.status === 429) {
throw new Error(`HTTP ${response.status}`);
}
return response;
} catch (error) {
lastError = error;
if (attempt < maxRetries) {
const delay = Math.min(1000 * Math.pow(2, attempt), 10000); // Max 10s
console.log(`Retry ${attempt + 1}/${maxRetries} after ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw lastError;
}
// Usage
try {
const response = await fetchWithRetry('/api/users');
const data = await response.json();
} catch (error) {
console.error('All retries failed:', error);
}
// Exponential Backoff with Jitter
function exponentialBackoffWithJitter(attempt, baseDelay = 1000, maxDelay = 30000) {
const exponentialDelay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
const jitter = Math.random() * exponentialDelay;
return Math.floor(jitter);
}
// Axios Retry Plugin
import axios from 'axios';
import axiosRetry from 'axios-retry';
const client = axios.create();
axiosRetry(client, {
retries: 3,
retryDelay: axiosRetry.exponentialDelay,
retryCondition: (error) => {
return axiosRetry.isNetworkOrIdempotentRequestError(error) ||
error.response?.status === 429;
}
});
// Custom retry with condition
axiosRetry(client, {
retries: 5,
retryDelay: (retryCount) => {
return retryCount * 1000; // Linear backoff
},
retryCondition: (error) => {
return error.response?.status >= 500;
},
onRetry: (retryCount, error, requestConfig) => {
console.log(`Retry attempt ${retryCount} for ${requestConfig.url}`);
}
});
// React Query Retry Configuration
import { useQuery } from '@tanstack/react-query';
function Users() {
const { data } = useQuery({
queryKey: ['users'],
queryFn: () => api.get('/users'),
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000)
});
}
// Circuit Breaker Pattern
class CircuitBreaker {
constructor(threshold = 5, timeout = 60000) {
this.failureThreshold = threshold;
this.timeout = timeout;
this.failureCount = 0;
this.lastFailureTime = null;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
}
async execute(fn) {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailureTime > this.timeout) {
this.state = 'HALF_OPEN';
} else {
throw new Error('Circuit breaker is OPEN');
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failureCount = 0;
this.state = 'CLOSED';
}
onFailure() {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.failureThreshold) {
this.state = 'OPEN';
console.log('Circuit breaker opened');
}
}
}
// Usage
const breaker = new CircuitBreaker(5, 60000);
async function fetchData() {
return breaker.execute(async () => {
const response = await fetch('/api/data');
if (!response.ok) throw new Error('Request failed');
return response.json();
});
}
// Rate Limiting (Frontend)
class RateLimiter {
constructor(maxRequests, timeWindow) {
this.maxRequests = maxRequests;
this.timeWindow = timeWindow;
this.requests = [];
}
async throttle(fn) {
const now = Date.now();
this.requests = this.requests.filter(time => now - time < this.timeWindow);
if (this.requests.length >= this.maxRequests) {
const oldestRequest = this.requests[0];
const waitTime = this.timeWindow - (now - oldestRequest);
await new Promise(resolve => setTimeout(resolve, waitTime));
return this.throttle(fn);
}
this.requests.push(now);
return fn();
}
}
// Usage: Max 10 requests per minute
const limiter = new RateLimiter(10, 60000);
async function fetchWithRateLimit(url) {
return limiter.throttle(() => fetch(url));
}
// Handle 429 Rate Limit Response
async function fetchWithRateLimitHandling(url) {
const response = await fetch(url);
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
const delay = retryAfter ? parseInt(retryAfter) * 1000 : 60000;
console.log(`Rate limited. Retrying after ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
return fetchWithRateLimitHandling(url);
}
return response;
}
API Integration Summary
- Modern Fetching: Use TanStack Query or SWR for automatic caching and revalidation
- GraphQL: Apollo Client for complex data requirements, avoid over-fetching
- Type Safety: tRPC for full-stack TypeScript, zero code generation
- Offline: IndexedDB with Dexie.js, sync queue when online
- Mocking: MSW for realistic API mocks in dev and test
- Resilience: Exponential backoff with jitter, circuit breaker pattern
Don't Retry Everything: Only retry idempotent operations (GET,
PUT, DELETE). Never auto-retry POST requests as they may create duplicates.