API Integration Modern Implementation

1. Axios Fetch API Error Handling

Feature Axios Fetch API Advantage
HTTP Client Third-party library Native browser API Fetch: No dependency
Error Handling Automatic error throwing Manual response.ok check Axios: Simpler errors
Request/Response Interceptors Built-in Manual wrapper needed Axios: More features
JSON Parsing Automatic Manual .json() call Axios: Convenience
Timeout Built-in timeout option AbortController needed Axios: Native timeout
Request Cancellation CancelToken / AbortController AbortController Both support cancellation

Example: Axios vs Fetch API with comprehensive error handling

// Install Axios
npm install axios

// Axios configuration with interceptors
import axios from 'axios';

const apiClient = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  },
});

// Request interceptor
apiClient.interceptors.request.use(
  (config) => {
    // Add auth token
    const token = localStorage.getItem('token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    
    // Add timestamp
    config.metadata = { startTime: new Date() };
    
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// Response interceptor
apiClient.interceptors.response.use(
  (response) => {
    // Log response time
    const endTime = new Date();
    const duration = endTime - response.config.metadata.startTime;
    console.log(`Request to ${response.config.url} took ${duration}ms`);
    
    return response.data;
  },
  async (error) => {
    const originalRequest = error.config;

    // Handle 401 - Refresh token
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;

      try {
        const { data } = await axios.post('/auth/refresh');
        localStorage.setItem('token', data.token);
        
        originalRequest.headers.Authorization = `Bearer ${data.token}`;
        return apiClient(originalRequest);
      } catch (refreshError) {
        // Redirect to login
        window.location.href = '/login';
        return Promise.reject(refreshError);
      }
    }

    // Handle different error types
    if (error.response) {
      // Server responded with error status
      console.error('Response error:', {
        status: error.response.status,
        data: error.response.data,
        headers: error.response.headers,
      });
    } else if (error.request) {
      // Request made but no response
      console.error('Network error:', error.request);
    } else {
      // Error in request setup
      console.error('Request setup error:', error.message);
    }

    return Promise.reject(error);
  }
);

// Usage examples
async function fetchUsers() {
  try {
    const users = await apiClient.get('/users');
    return users;
  } catch (error) {
    if (axios.isAxiosError(error)) {
      if (error.code === 'ECONNABORTED') {
        console.error('Request timeout');
      } else if (error.response?.status === 404) {
        console.error('Users not found');
      }
    }
    throw error;
  }
}

// Parallel requests
async function fetchDashboardData() {
  try {
    const [users, posts, comments] = await Promise.all([
      apiClient.get('/users'),
      apiClient.get('/posts'),
      apiClient.get('/comments'),
    ]);
    
    return { users, posts, comments };
  } catch (error) {
    console.error('Dashboard fetch failed:', error);
    throw error;
  }
}

// Request cancellation with Axios
const controller = new AbortController();

apiClient.get('/users', {
  signal: controller.signal,
})
.then(data => console.log(data))
.catch(error => {
  if (error.code === 'ERR_CANCELED') {
    console.log('Request cancelled');
  }
});

// Cancel the request
controller.abort();

// Fetch API with error handling
async function fetchWithErrorHandling(url, options = {}) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 10000);

  try {
    const response = await fetch(url, {
      ...options,
      signal: controller.signal,
      headers: {
        'Content-Type': 'application/json',
        ...options.headers,
      },
    });

    clearTimeout(timeoutId);

    // Check for HTTP errors
    if (!response.ok) {
      const errorData = await response.json().catch(() => ({}));
      throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
    }

    const data = await response.json();
    return data;
  } catch (error) {
    clearTimeout(timeoutId);

    if (error.name === 'AbortError') {
      throw new Error('Request timeout');
    }

    throw error;
  }
}

// Fetch wrapper with interceptors
class APIClient {
  constructor(baseURL) {
    this.baseURL = baseURL;
    this.requestInterceptors = [];
    this.responseInterceptors = [];
  }

  addRequestInterceptor(fn) {
    this.requestInterceptors.push(fn);
  }

  addResponseInterceptor(fn) {
    this.responseInterceptors.push(fn);
  }

  async request(endpoint, options = {}) {
    let url = `${this.baseURL}${endpoint}`;
    let config = { ...options };

    // Apply request interceptors
    for (const interceptor of this.requestInterceptors) {
      const result = await interceptor({ url, config });
      url = result.url;
      config = result.config;
    }

    try {
      const response = await fetch(url, config);

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }

      let data = await response.json();

      // Apply response interceptors
      for (const interceptor of this.responseInterceptors) {
        data = await interceptor(data);
      }

      return data;
    } catch (error) {
      console.error('Request failed:', error);
      throw error;
    }
  }

  get(endpoint, options) {
    return this.request(endpoint, { ...options, method: 'GET' });
  }

  post(endpoint, body, options) {
    return this.request(endpoint, {
      ...options,
      method: 'POST',
      body: JSON.stringify(body),
      headers: {
        'Content-Type': 'application/json',
        ...options?.headers,
      },
    });
  }
}

// Usage
const api = new APIClient('https://api.example.com');

api.addRequestInterceptor(({ url, config }) => {
  const token = localStorage.getItem('token');
  if (token) {
    config.headers = {
      ...config.headers,
      Authorization: `Bearer ${token}`,
    };
  }
  return { url, config };
});

api.addResponseInterceptor((data) => {
  console.log('Response received:', data);
  return data;
});

// React hook for API calls
import { useState, useEffect } from 'react';

function useAPI(url, options = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    async function fetchData() {
      try {
        setLoading(true);
        const response = await apiClient.get(url, {
          ...options,
          signal: controller.signal,
        });
        setData(response);
        setError(null);
      } catch (err) {
        if (err.name !== 'CanceledError') {
          setError(err);
        }
      } finally {
        setLoading(false);
      }
    }

    fetchData();

    return () => controller.abort();
  }, [url]);

  return { data, loading, error };
}

// Usage
function UsersList() {
  const { data: users, loading, error } = useAPI('/users');

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

2. GraphQL Apollo Client Codegen

Feature Implementation Benefit Tool
Apollo Client GraphQL client library Caching, state management @apollo/client
Code Generation Types from schema Type safety, autocomplete graphql-codegen
Type-safe Hooks Generated React hooks Typed queries/mutations @graphql-codegen/typescript-react-apollo
Normalized Cache Automatic data normalization Efficient updates, consistency InMemoryCache
Optimistic Updates Update UI before response Better UX optimisticResponse option
Error Handling GraphQL + network errors Granular error info onError link

Example: GraphQL with Apollo Client and code generation

// Install dependencies
npm install @apollo/client graphql
npm install --save-dev @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo

// codegen.yml configuration
schema: 'https://api.example.com/graphql'
documents: 'src/**/*.graphql'
generates:
  src/generated/graphql.ts:
    plugins:
      - typescript
      - typescript-operations
      - typescript-react-apollo
    config:
      withHooks: true
      withHOC: false
      withComponent: false

// package.json scripts
{
  "scripts": {
    "codegen": "graphql-codegen --config codegen.yml",
    "codegen:watch": "graphql-codegen --config codegen.yml --watch"
  }
}

// GraphQL queries (src/queries/users.graphql)
query GetUsers {
  users {
    id
    name
    email
    avatar
  }
}

query GetUser($id: ID!) {
  user(id: $id) {
    id
    name
    email
    posts {
      id
      title
      content
    }
  }
}

mutation CreateUser($input: CreateUserInput!) {
  createUser(input: $input) {
    id
    name
    email
  }
}

mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
  updateUser(id: $id, input: $input) {
    id
    name
    email
  }
}

// Apollo Client setup
import { ApolloClient, InMemoryCache, ApolloProvider, createHttpLink } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import { onError } from '@apollo/client/link/error';

// HTTP link
const httpLink = createHttpLink({
  uri: 'https://api.example.com/graphql',
});

// Auth link
const authLink = setContext((_, { headers }) => {
  const token = localStorage.getItem('token');
  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : '',
    },
  };
});

// Error link
const errorLink = onError(({ graphQLErrors, networkError }) => {
  if (graphQLErrors) {
    graphQLErrors.forEach(({ message, locations, path }) => {
      console.error(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`);
    });
  }

  if (networkError) {
    console.error(`[Network error]: ${networkError}`);
  }
});

// Create Apollo Client
const client = new ApolloClient({
  link: errorLink.concat(authLink.concat(httpLink)),
  cache: new InMemoryCache({
    typePolicies: {
      User: {
        keyFields: ['id'],
      },
      Post: {
        keyFields: ['id'],
      },
    },
  }),
  defaultOptions: {
    watchQuery: {
      fetchPolicy: 'cache-and-network',
      errorPolicy: 'all',
    },
    query: {
      fetchPolicy: 'network-only',
      errorPolicy: 'all',
    },
    mutate: {
      errorPolicy: 'all',
    },
  },
});

// App with ApolloProvider
function App() {
  return (
    <ApolloProvider client={client}>
      <YourApp />
    </ApolloProvider>
  );
}

// Use generated hooks (TypeScript)
import { useGetUsersQuery, useGetUserQuery, useCreateUserMutation } from './generated/graphql';

function UsersList() {
  const { data, loading, error, refetch } = useGetUsersQuery({
    pollInterval: 5000, // Poll every 5 seconds
  });

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <button onClick={() => refetch()}>Refresh</button>
      <ul>
        {data?.users.map(user => (
          <li key={user.id}>{user.name} - {user.email}</li>
        ))}
      </ul>
    </div>
  );
}

// Single user with variables
function UserProfile({ userId }: { userId: string }) {
  const { data, loading, error } = useGetUserQuery({
    variables: { id: userId },
    skip: !userId, // Skip query if no userId
  });

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!data?.user) return <div>User not found</div>;

  return (
    <div>
      <h1>{data.user.name}</h1>
      <p>{data.user.email}</p>
      <h2>Posts</h2>
      <ul>
        {data.user.posts.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
}

// Mutation with optimistic response
function CreateUserForm() {
  const [createUser, { loading, error }] = useCreateUserMutation({
    update(cache, { data }) {
      // Update cache after mutation
      cache.modify({
        fields: {
          users(existingUsers = []) {
            const newUserRef = cache.writeFragment({
              data: data?.createUser,
              fragment: gql`
                fragment NewUser on User {
                  id
                  name
                  email
                }
              `,
            });
            return [...existingUsers, newUserRef];
          },
        },
      });
    },
    optimisticResponse: {
      createUser: {
        __typename: 'User',
        id: 'temp-id',
        name: 'Loading...',
        email: 'loading@example.com',
      },
    },
  });

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    const formData = new FormData(e.target as HTMLFormElement);

    try {
      await createUser({
        variables: {
          input: {
            name: formData.get('name') as string,
            email: formData.get('email') as string,
          },
        },
      });
    } catch (err) {
      console.error('Mutation error:', err);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" placeholder="Name" required />
      <input name="email" type="email" placeholder="Email" required />
      <button type="submit" disabled={loading}>
        {loading ? 'Creating...' : 'Create User'}
      </button>
      {error && <div>Error: {error.message}</div>}
    </form>
  );
}

// Lazy query (load on demand)
import { useGetUserLazyQuery } from './generated/graphql';

function SearchUser() {
  const [getUser, { data, loading }] = useGetUserLazyQuery();

  const handleSearch = (userId: string) => {
    getUser({ variables: { id: userId } });
  };

  return (
    <div>
      <button onClick={() => handleSearch('123')}>Load User</button>
      {loading && <div>Loading...</div>}
      {data && <div>{data.user?.name}</div>}
    </div>
  );
}

// Subscription example
subscription OnUserCreated {
  userCreated {
    id
    name
    email
  }
}

// Use subscription
import { useOnUserCreatedSubscription } from './generated/graphql';

function RealtimeUsers() {
  const { data } = useOnUserCreatedSubscription();

  useEffect(() => {
    if (data?.userCreated) {
      console.log('New user created:', data.userCreated);
    }
  }, [data]);

  return <div>Listening for new users...</div>;
}

// Fragment colocation
const USER_FRAGMENT = gql`
  fragment UserFields on User {
    id
    name
    email
    avatar
  }
`;

const GET_USERS_WITH_FRAGMENT = gql`
  ${USER_FRAGMENT}
  query GetUsers {
    users {
      ...UserFields
    }
  }
`;

// Cache manipulation
import { useApolloClient } from '@apollo/client';

function UserActions() {
  const client = useApolloClient();

  const updateUserInCache = (userId: string, updates: Partial<User>) => {
    client.cache.modify({
      id: client.cache.identify({ __typename: 'User', id: userId }),
      fields: {
        name: () => updates.name,
        email: () => updates.email,
      },
    });
  };

  return <button onClick={() => updateUserInCache('1', { name: 'Updated' })}>Update</button>;
}

3. tRPC Type-safe API Calls

Feature Description Benefit Use Case
End-to-end Type Safety TypeScript from server to client No manual type definitions Full-stack TypeScript apps
No Code Generation Direct type inference Simpler setup vs GraphQL Monorepo projects
React Query Integration Built on @tanstack/react-query Caching, optimistic updates Modern React apps
Zod Validation Runtime input validation Type-safe validation Robust API contracts
Lightweight Small bundle size Better performance Bundle-conscious apps
Autocomplete Full IDE support Developer experience All TypeScript projects

Example: tRPC full-stack type-safe implementation

// Install tRPC
npm install @trpc/server @trpc/client @trpc/react-query @trpc/next @tanstack/react-query zod

// Server setup (server/trpc.ts)
import { initTRPC } from '@trpc/server';
import { z } from 'zod';

// Create tRPC context
export const createContext = async ({ req, res }) => {
  return {
    user: req.user,
    prisma: prisma, // or your database client
  };
};

type Context = Awaited<ReturnType<typeof createContext>>;

// Initialize tRPC
const t = initTRPC.context<Context>().create();

// Export reusable router and procedure helpers
export const router = t.router;
export const publicProcedure = t.procedure;

// Protected procedure with auth middleware
const isAuthed = t.middleware(({ ctx, next }) => {
  if (!ctx.user) {
    throw new Error('Not authenticated');
  }
  return next({
    ctx: {
      user: ctx.user,
    },
  });
});

export const protectedProcedure = t.procedure.use(isAuthed);

// Define router (server/routers/user.ts)
import { router, publicProcedure, protectedProcedure } from '../trpc';
import { z } from 'zod';

const userRouter = router({
  // Query - get all users
  getAll: publicProcedure
    .query(async ({ ctx }) => {
      return await ctx.prisma.user.findMany();
    }),

  // Query with input - get user by ID
  getById: publicProcedure
    .input(z.object({
      id: z.string(),
    }))
    .query(async ({ ctx, input }) => {
      return await ctx.prisma.user.findUnique({
        where: { id: input.id },
      });
    }),

  // Mutation - create user
  create: protectedProcedure
    .input(z.object({
      name: z.string().min(3).max(50),
      email: z.string().email(),
      age: z.number().int().min(18).optional(),
    }))
    .mutation(async ({ ctx, input }) => {
      return await ctx.prisma.user.create({
        data: input,
      });
    }),

  // Mutation - update user
  update: protectedProcedure
    .input(z.object({
      id: z.string(),
      name: z.string().min(3).max(50).optional(),
      email: z.string().email().optional(),
    }))
    .mutation(async ({ ctx, input }) => {
      const { id, ...data } = input;
      return await ctx.prisma.user.update({
        where: { id },
        data,
      });
    }),

  // Mutation - delete user
  delete: protectedProcedure
    .input(z.object({
      id: z.string(),
    }))
    .mutation(async ({ ctx, input }) => {
      return await ctx.prisma.user.delete({
        where: { id: input.id },
      });
    }),
});

// Main app router (server/routers/index.ts)
import { router } from '../trpc';
import { userRouter } from './user';
import { postRouter } from './post';

export const appRouter = router({
  user: userRouter,
  post: postRouter,
});

export type AppRouter = typeof appRouter;

// Next.js API route (pages/api/trpc/[trpc].ts)
import { createNextApiHandler } from '@trpc/server/adapters/next';
import { appRouter } from '../../../server/routers';
import { createContext } from '../../../server/trpc';

export default createNextApiHandler({
  router: appRouter,
  createContext,
  onError: ({ path, error }) => {
    console.error(`tRPC Error on '${path}':`, error);
  },
});

// Client setup (utils/trpc.ts)
import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import type { AppRouter } from '../server/routers';

export const trpc = createTRPCNext<AppRouter>({
  config({ ctx }) {
    return {
      links: [
        httpBatchLink({
          url: '/api/trpc',
          headers() {
            return {
              authorization: `Bearer ${localStorage.getItem('token')}`,
            };
          },
        }),
      ],
      queryClientConfig: {
        defaultOptions: {
          queries: {
            staleTime: 60 * 1000, // 1 minute
          },
        },
      },
    };
  },
  ssr: false,
});

// Wrap app with tRPC provider (pages/_app.tsx)
import { trpc } from '../utils/trpc';
import type { AppProps } from 'next/app';

function MyApp({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />;
}

export default trpc.withTRPC(MyApp);

// Use tRPC in components - fully type-safe!
import { trpc } from '../utils/trpc';

function UsersList() {
  // Query - automatically typed!
  const { data: users, isLoading, error } = trpc.user.getAll.useQuery();

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <ul>
      {users?.map(user => (
        <li key={user.id}>{user.name} - {user.email}</li>
      ))}
    </ul>
  );
}

// Query with input
function UserProfile({ userId }: { userId: string }) {
  const { data: user } = trpc.user.getById.useQuery(
    { id: userId },
    { enabled: !!userId } // Only fetch if userId exists
  );

  return <div>{user?.name}</div>;
}

// Mutation
function CreateUserForm() {
  const utils = trpc.useContext();
  
  const createUser = trpc.user.create.useMutation({
    onSuccess: () => {
      // Invalidate and refetch users list
      utils.user.getAll.invalidate();
    },
    onError: (error) => {
      console.error('Failed to create user:', error);
    },
  });

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);

    createUser.mutate({
      name: formData.get('name') as string,
      email: formData.get('email') as string,
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" required />
      <input name="email" type="email" required />
      <button type="submit" disabled={createUser.isLoading}>
        {createUser.isLoading ? 'Creating...' : 'Create'}
      </button>
      {createUser.error && <div>{createUser.error.message}</div>}
    </form>
  );
}

// Optimistic updates
function UpdateUserForm({ userId }: { userId: string }) {
  const utils = trpc.useContext();

  const updateUser = trpc.user.update.useMutation({
    onMutate: async (newData) => {
      // Cancel outgoing refetches
      await utils.user.getById.cancel({ id: userId });

      // Snapshot previous value
      const previousUser = utils.user.getById.getData({ id: userId });

      // Optimistically update
      utils.user.getById.setData({ id: userId }, (old) => ({
        ...old!,
        ...newData,
      }));

      return { previousUser };
    },
    onError: (err, newData, context) => {
      // Rollback on error
      utils.user.getById.setData({ id: userId }, context?.previousUser);
    },
    onSettled: () => {
      // Refetch after error or success
      utils.user.getById.invalidate({ id: userId });
    },
  });

  return <div>...</div>;
}

// Prefetch for faster navigation
function UserLink({ userId }: { userId: string }) {
  const utils = trpc.useContext();

  const handleMouseEnter = () => {
    // Prefetch user data on hover
    utils.user.getById.prefetch({ id: userId });
  };

  return (
    <a href={`/users/${userId}`} onMouseEnter={handleMouseEnter}>
      View User
    </a>
  );
}

// Infinite query for pagination
const infiniteUserRouter = router({
  getInfinite: publicProcedure
    .input(z.object({
      limit: z.number().min(1).max(100).default(10),
      cursor: z.string().optional(),
    }))
    .query(async ({ ctx, input }) => {
      const users = await ctx.prisma.user.findMany({
        take: input.limit + 1,
        cursor: input.cursor ? { id: input.cursor } : undefined,
      });

      let nextCursor: string | undefined;
      if (users.length > input.limit) {
        const nextItem = users.pop();
        nextCursor = nextItem!.id;
      }

      return {
        users,
        nextCursor,
      };
    }),
});

// Use infinite query
function InfiniteUsersList() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = trpc.user.getInfinite.useInfiniteQuery(
    { limit: 10 },
    {
      getNextPageParam: (lastPage) => lastPage.nextCursor,
    }
  );

  return (
    <div>
      {data?.pages.map((page, i) => (
        <div key={i}>
          {page.users.map(user => (
            <div key={user.id}>{user.name}</div>
          ))}
        </div>
      ))}
      
      {hasNextPage && (
        <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
          {isFetchingNextPage ? 'Loading...' : 'Load More'}
        </button>
      )}
    </div>
  );
}

4. React Query Infinite Queries

Feature Hook Purpose Use Case
useQuery Fetch data GET requests, caching List, detail views
useMutation Modify data POST/PUT/DELETE Forms, updates
useInfiniteQuery Paginated data Load more pattern Infinite scroll
Optimistic Updates Update before response Instant UI feedback Likes, votes, edits
Cache Invalidation Refetch stale data Keep data fresh After mutations
Prefetching Load data early Faster navigation Hover, route changes

Example: React Query comprehensive implementation

// Install React Query
npm install @tanstack/react-query @tanstack/react-query-devtools

// Setup QueryClient (App.tsx)
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60 * 1000,
      cacheTime: 5 * 60 * 1000,
      refetchOnWindowFocus: false,
      retry: 3,
    },
  },
});

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

// useQuery - Basic fetch
import { useQuery } from '@tanstack/react-query';

function UsersList() {
  const { data: users, isLoading, error } = useQuery({
    queryKey: ['users'],
    queryFn: () => fetch('/api/users').then(res => res.json()),
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

// useMutation - POST/PUT/DELETE
import { useMutation, useQueryClient } from '@tanstack/react-query';

function CreateUserForm() {
  const queryClient = useQueryClient();

  const createUser = useMutation({
    mutationFn: (newUser) => {
      return fetch('/api/users', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(newUser),
      }).then(res => res.json());
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
  });

  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      createUser.mutate({ name: 'John', email: 'john@example.com' });
    }}>
      <button type="submit">Create</button>
    </form>
  );
}

// useInfiniteQuery - Pagination
import { useInfiniteQuery } from '@tanstack/react-query';

function InfinitePostsList() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: ['posts'],
    queryFn: ({ pageParam = 0 }) => {
      return fetch(`/api/posts?cursor=${pageParam}`).then(res => res.json());
    },
    getNextPageParam: (lastPage) => lastPage.nextCursor,
    initialPageParam: 0,
  });

  return (
    <div>
      {data?.pages.map((page, i) => (
        <div key={i}>
          {page.posts.map(post => (
            <div key={post.id}>{post.title}</div>
          ))}
        </div>
      ))}
      <button
        onClick={() => fetchNextPage()}
        disabled={!hasNextPage || isFetchingNextPage}
      >
        {isFetchingNextPage ? 'Loading...' : 'Load More'}
      </button>
    </div>
  );
}

// Optimistic Updates
function LikeButton({ postId }) {
  const queryClient = useQueryClient();

  const likeMutation = useMutation({
    mutationFn: (postId) => fetch(`/api/posts/${postId}/like`, { method: 'POST' }),
    onMutate: async (postId) => {
      await queryClient.cancelQueries({ queryKey: ['post', postId] });
      const previousPost = queryClient.getQueryData(['post', postId]);
      
      queryClient.setQueryData(['post', postId], (old) => ({
        ...old,
        likes: old.likes + 1,
      }));
      
      return { previousPost };
    },
    onError: (err, postId, context) => {
      queryClient.setQueryData(['post', postId], context.previousPost);
    },
  });

  return <button onClick={() => likeMutation.mutate(postId)}>Like</button>;
}

// Prefetch on hover
function PostLink({ postId }) {
  const queryClient = useQueryClient();

  return (
    <a
      href={`/posts/${postId}`}
      onMouseEnter={() => {
        queryClient.prefetchQuery({
          queryKey: ['post', postId],
          queryFn: () => fetch(`/api/posts/${postId}`).then(res => res.json()),
        });
      }}
    >
      View Post
    </a>
  );
}

5. MSW Mock Service Worker

Feature Purpose Environment Benefit
Service Worker API Intercept network requests Browser (dev/test) No code changes
Node.js Integration Mock in tests Jest, Vitest Test isolation
Type-safe Handlers TypeScript support All environments Catch errors early
REST & GraphQL Both API types Universal Flexible mocking
Stateful Mocking Simulate state changes Development Realistic behavior
Network Simulation Delays, errors Testing Edge case testing

Example: MSW for API mocking

// Install MSW
npm install msw --save-dev
npx msw init public/ --save

// Create handlers (src/mocks/handlers.ts)
import { http, HttpResponse, graphql } from 'msw';

export const handlers = [
  // REST handlers
  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' },
    ]);
  }),

  http.get('/api/users/:id', ({ params }) => {
    const { id } = params;
    return HttpResponse.json({
      id,
      name: 'John Doe',
      email: 'john@example.com',
    });
  }),

  http.post('/api/users', async ({ request }) => {
    const newUser = await request.json();
    return HttpResponse.json(
      { id: Math.random().toString(), ...newUser },
      { status: 201 }
    );
  }),

  // Simulate errors
  http.get('/api/error', () => {
    return HttpResponse.json(
      { error: 'Something went wrong' },
      { status: 500 }
    );
  }),

  // Simulate delay
  http.get('/api/slow', async () => {
    await delay(3000);
    return HttpResponse.json({ data: 'slow response' });
  }),

  // GraphQL handlers
  graphql.query('GetUsers', () => {
    return HttpResponse.json({
      data: {
        users: [
          { id: '1', name: 'John' },
          { id: '2', name: 'Jane' },
        ],
      },
    });
  }),
];

// Browser setup (src/mocks/browser.ts)
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';

export const worker = setupWorker(...handlers);

// Start in development (src/main.tsx)
async function enableMocking() {
  if (process.env.NODE_ENV !== 'development') return;

  const { worker } = await import('./mocks/browser');
  return worker.start();
}

enableMocking().then(() => {
  ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
});

// Test setup (src/mocks/server.ts)
import { setupServer } from 'msw/node';
import { handlers } from './handlers';

export const server = setupServer(...handlers);

// Setup tests (src/setupTests.ts)
import { beforeAll, afterEach, afterAll } from 'vitest';
import { server } from './mocks/server';

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

// Test with MSW
import { render, screen, waitFor } from '@testing-library/react';
import { server } from './mocks/server';
import { http, HttpResponse } from 'msw';

test('displays users', async () => {
  render(<UsersList />);
  
  await waitFor(() => {
    expect(screen.getByText('John Doe')).toBeInTheDocument();
  });
});

test('handles error', async () => {
  server.use(
    http.get('/api/users', () => {
      return HttpResponse.json({ error: 'Failed' }, { status: 500 });
    })
  );

  render(<UsersList />);
  await waitFor(() => {
    expect(screen.getByText(/error/i)).toBeInTheDocument();
  });
});

// Stateful mocking with database
import { factory, primaryKey } from '@mswjs/data';

const db = factory({
  user: {
    id: primaryKey(String),
    name: String,
    email: String,
  },
});

db.user.create({ id: '1', name: 'John', email: 'john@example.com' });

export const statefulHandlers = [
  http.get('/api/users', () => {
    return HttpResponse.json(db.user.getAll());
  }),
  
  http.post('/api/users', async ({ request }) => {
    const newUser = await request.json();
    const user = db.user.create({ id: Math.random().toString(), ...newUser });
    return HttpResponse.json(user, { status: 201 });
  }),
];

6. API Rate Limiting Retry Logic

Strategy Implementation Use Case Consideration
Exponential Backoff Delay *= 2 after each retry 429 rate limit errors Avoid thundering herd
Retry-After Header Respect server's retry time Server-specified delays Most accurate timing
Jitter Add randomness to delay Prevent synchronized retries Better distribution
Circuit Breaker Stop after threshold failures Protect failing services Fail fast when needed
Request Queue Throttle outgoing requests Client-side rate limiting Prevent hitting limits
Token Bucket Allow burst then limit Smooth traffic patterns Balance speed and limits

Example: Advanced retry logic and rate limiting

// Exponential backoff with jitter
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);

      if (response.ok) return response;

      if (response.status === 429 || response.status >= 500) {
        const retryAfter = response.headers.get('Retry-After');
        let delay;

        if (retryAfter) {
          delay = parseInt(retryAfter) * 1000;
        } else {
          const exponentialDelay = Math.pow(2, attempt) * 1000;
          const jitter = Math.random() * 1000;
          delay = exponentialDelay + jitter;
        }

        await new Promise(resolve => setTimeout(resolve, delay));
        continue;
      }

      throw new Error(`HTTP ${response.status}`);
    } catch (error) {
      if (attempt === maxRetries) throw error;
    }
  }
}

// Circuit Breaker
class CircuitBreaker {
  constructor(threshold = 5, timeout = 60000) {
    this.failures = 0;
    this.threshold = threshold;
    this.timeout = timeout;
    this.state = 'CLOSED';
    this.lastFailureTime = null;
  }

  async execute(fn) {
    if (this.state === 'OPEN') {
      const timeSinceFailure = Date.now() - this.lastFailureTime;
      if (timeSinceFailure > this.timeout) {
        this.state = 'HALF_OPEN';
      } else {
        throw new Error('Circuit breaker is OPEN');
      }
    }

    try {
      const result = await fn();
      if (this.state === 'HALF_OPEN') {
        this.state = 'CLOSED';
        this.failures = 0;
      }
      return result;
    } catch (error) {
      this.failures++;
      this.lastFailureTime = Date.now();
      if (this.failures >= this.threshold) {
        this.state = 'OPEN';
      }
      throw error;
    }
  }
}

const breaker = new CircuitBreaker();
await breaker.execute(() => fetch('/api/data'));

// Request Queue with Rate Limiting
class RequestQueue {
  constructor(maxRequestsPerWindow = 10, windowMs = 1000) {
    this.queue = [];
    this.processing = false;
    this.requestsInWindow = 0;
    this.windowStart = Date.now();
    this.maxRequestsPerWindow = maxRequestsPerWindow;
    this.windowMs = windowMs;
  }

  async add(fn) {
    return new Promise((resolve, reject) => {
      this.queue.push(async () => {
        try {
          resolve(await fn());
        } catch (error) {
          reject(error);
        }
      });
      this.process();
    });
  }

  async process() {
    if (this.processing || this.queue.length === 0) return;
    this.processing = true;

    while (this.queue.length > 0) {
      const now = Date.now();
      const elapsed = now - this.windowStart;

      if (elapsed >= this.windowMs) {
        this.requestsInWindow = 0;
        this.windowStart = now;
      }

      if (this.requestsInWindow >= this.maxRequestsPerWindow) {
        await new Promise(resolve => setTimeout(resolve, this.windowMs - elapsed));
        continue;
      }

      const fn = this.queue.shift();
      this.requestsInWindow++;
      await fn();
    }

    this.processing = false;
  }
}

const queue = new RequestQueue(10, 1000);
for (let i = 0; i < 100; i++) {
  queue.add(() => fetch(`/api/data/${i}`));
}

// Token Bucket
class TokenBucket {
  constructor(capacity, refillRate) {
    this.tokens = capacity;
    this.capacity = capacity;
    this.refillRate = refillRate;
    this.lastRefill = Date.now();
  }

  async consume(tokens = 1) {
    this.refill();

    if (this.tokens >= tokens) {
      this.tokens -= tokens;
      return;
    }

    const tokensNeeded = tokens - this.tokens;
    const waitTime = (tokensNeeded / this.refillRate) * 1000;
    await new Promise(resolve => setTimeout(resolve, waitTime));
    
    this.refill();
    this.tokens -= tokens;
  }

  refill() {
    const now = Date.now();
    const elapsed = (now - this.lastRefill) / 1000;
    const tokensToAdd = elapsed * this.refillRate;
    this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
    this.lastRefill = now;
  }
}

const bucket = new TokenBucket(10, 2);
await bucket.consume(1);
await fetch('/api/data');

// React Query with retry
import { useQuery } from '@tanstack/react-query';

function UserData({ userId }) {
  const { data } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetch(`/api/users/${userId}`).then(res => res.json()),
    retry: (failureCount, error) => {
      if (error.response?.status === 404) return false;
      return failureCount < 3;
    },
    retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
  });

  return <div>{data?.name}</div>;
}

API Integration Best Practices Summary

  • Axios vs Fetch - Axios provides interceptors, automatic JSON parsing, built-in timeout; Fetch is native, needs manual error/timeout handling
  • GraphQL Apollo - Use @graphql-codegen for type-safe hooks, normalize cache, implement optimistic updates, leverage fragments
  • tRPC Type Safety - End-to-end TypeScript without codegen, Zod validation, built on React Query, automatic autocomplete
  • React Query Patterns - useQuery for fetching, useMutation for updates, useInfiniteQuery for pagination, optimistic updates, prefetch on hover
  • MSW Mocking - Mock APIs in browser with Service Worker, stateful mocking, test without backend, simulate delays/errors
  • Retry & Rate Limiting - Exponential backoff with jitter, respect Retry-After headers, circuit breaker, request queue, token bucket
  • Production Ready - Comprehensive error handling, automatic token refresh, request cancellation, proper TypeScript types, monitoring