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>
  );
}

Fetch vs Axios

Feature Fetch Axios
Built-in ✅ Yes ❌ External
JSON Transform Manual ✅ Automatic
Interceptors ❌ No ✅ Yes
Timeout AbortController ✅ Built-in
Progress Events Manual ✅ Built-in

HTTP Methods

  • GET: Retrieve resources
  • POST: Create new resources
  • PUT: Replace entire resource
  • PATCH: Partial update
  • DELETE: Remove resource
  • HEAD: Get headers only
  • OPTIONS: Check allowed methods
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!
}

GraphQL vs REST

Feature REST GraphQL
Over-fetching ❌ Common ✅ None
Multiple Endpoints ✅ Many ❌ Single
Type Safety Manual ✅ Built-in
Caching HTTP cache Normalized

Apollo Cache Policies

  • cache-first: Use cache, fetch if missing
  • cache-only: Never fetch, cache only
  • network-only: Always fetch, update cache
  • no-cache: Fetch without cache
  • cache-and-network: Return cache, fetch update
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 Benefits

  • ✅ Full type safety, no codegen
  • ✅ Autocomplete everywhere
  • ✅ Catch errors at compile time
  • ✅ Built on React Query
  • ✅ Minimal boilerplate
  • ✅ Excellent DX
  • ✅ Works with monorepos

When to Use tRPC

Scenario Use tRPC?
Full TypeScript stack ✅ Perfect
Monorepo ✅ Excellent
Public API ❌ Use REST/GraphQL
Mobile apps ⚠️ Use REST/GraphQL
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/ } }
});

Storage Comparison

API Size Performance
IndexedDB ~GB Fast, async
localStorage 5-10 MB Slow, blocking
sessionStorage 5-10 MB Slow, blocking
Cache API Varies Fast, async

IndexedDB Libraries

  • Dexie.js: Simple API, live queries, best DX
  • PouchDB: CouchDB sync, replication
  • idb: Promise wrapper, minimal
  • localForage: localStorage API, IndexedDB backend
  • RxDB: Reactive database, observables
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 Benefits

  • ✅ Works in browser and Node.js
  • ✅ No server needed
  • ✅ Network-level interception
  • ✅ Same code for dev and test
  • ✅ Realistic request/response
  • ✅ TypeScript support

When to Mock APIs

  • Backend not ready yet
  • Testing error scenarios
  • Offline development
  • Demos and prototypes
  • Integration tests
  • Slow API responses
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;
}

Retry Best Practices

Backoff Comparison

Attempt Linear (1s) Exponential (1s)
1 1s 1s
2 2s 2s
3 3s 4s
4 4s 8s
5 5s 16s

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.