DailyDevDiet

logo - dailydevdiet

Learn. Build. Innovate. Elevate your coding skills with dailydevdiet!

Chapter 32: Working with APIs

Working with APIs

Working with APIs is a fundamental aspect of modern React applications. This chapter covers various techniques for fetching, caching, and managing API data, from basic fetch operations to advanced patterns using modern libraries and custom hooks.

Understanding APIs in React Context

APIs (Application Programming Interfaces) allow React applications to communicate with backend services, third-party services, and external data sources. Common API operations include:

  • GET: Retrieving data
  • POST: Creating new resources
  • PUT/PATCH: Updating existing resources
  • DELETE: Removing resources

Basic API Integration with Fetch

Simple Data Fetching

import React, { useState, useEffect } from 'react';

const UserList = () => {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchUsers = async () => {
      try {
        setLoading(true);
        const response = await fetch('https://jsonplaceholder.typicode.com/users');
       
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
       
        const userData = await response.json();
        setUsers(userData);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchUsers();
  }, []);

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

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

export default UserList;

CRUD Operations

import React, { useState, useEffect } from 'react';

const PostManager = () => {
  const [posts, setPosts] = useState([]);
  const [newPost, setNewPost] = useState({ title: '', body: '' });
  const [editingPost, setEditingPost] = useState(null);
  const [loading, setLoading] = useState(false);

  const API_BASE = 'https://jsonplaceholder.typicode.com';

  // Fetch all posts
  const fetchPosts = async () => {
    setLoading(true);
    try {
      const response = await fetch(`${API_BASE}/posts`);
      const postsData = await response.json();
      setPosts(postsData.slice(0, 10)); // Limit to 10 posts for demo
    } catch (error) {
      console.error('Error fetching posts:', error);
    } finally {
      setLoading(false);
    }
  };

  // Create new post
  const createPost = async (postData) => {
    try {
      const response = await fetch(`${API_BASE}/posts`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(postData),
      });
     
      const newPostData = await response.json();
      setPosts(prev => [newPostData, ...prev]);
      setNewPost({ title: '', body: '' });
    } catch (error) {
      console.error('Error creating post:', error);
    }
  };

  // Update post
  const updatePost = async (id, postData) => {
    try {
      const response = await fetch(`${API_BASE}/posts/${id}`, {
        method: 'PUT',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(postData),
      });
     
      const updatedPost = await response.json();
      setPosts(prev => prev.map(post =>
        post.id === id ? updatedPost : post
      ));
      setEditingPost(null);
    } catch (error) {
      console.error('Error updating post:', error);
    }
  };

  // Delete post
  const deletePost = async (id) => {
    try {
      await fetch(`${API_BASE}/posts/${id}`, {
        method: 'DELETE',
      });
     
      setPosts(prev => prev.filter(post => post.id !== id));
    } catch (error) {
      console.error('Error deleting post:', error);
    }
  };

  useEffect(() => {
    fetchPosts();
  }, []);

  const handleSubmit = (e) => {
    e.preventDefault();
    if (newPost.title && newPost.body) {
      createPost(newPost);
    }
  };

  const handleUpdate = (e) => {
    e.preventDefault();
    if (editingPost) {
      updatePost(editingPost.id, editingPost);
    }
  };

  return (
    <div className="post-manager">
      <h2>Post Manager</h2>
     
      {/* Create new post form */}
      <form onSubmit={handleSubmit} className="post-form">
        <h3>Create New Post</h3>
        <input
          type="text"
          placeholder="Title"
          value={newPost.title}
          onChange={(e) => setNewPost({...newPost, title: e.target.value})}
        />
        <textarea
          placeholder="Body"
          value={newPost.body}
          onChange={(e) => setNewPost({...newPost, body: e.target.value})}
        />
        <button type="submit">Create Post</button>
      </form>

      {/* Edit post form */}
      {editingPost && (
        <form onSubmit={handleUpdate} className="post-form">
          <h3>Edit Post</h3>
          <input
            type="text"
            value={editingPost.title}
            onChange={(e) => setEditingPost({...editingPost, title: e.target.value})}
          />
          <textarea
            value={editingPost.body}
            onChange={(e) => setEditingPost({...editingPost, body: e.target.value})}
          />
          <button type="submit">Update Post</button>
          <button type="button" onClick={() => setEditingPost(null)}>
            Cancel
          </button>
        </form>
      )}

      {/* Posts list */}
      {loading ? (
        <div>Loading posts...</div>
      ) : (
        <div className="posts-list">
          {posts.map(post => (
            <div key={post.id} className="post-item">
              <h4>{post.title}</h4>
              <p>{post.body}</p>
              <div className="post-actions">
                <button onClick={() => setEditingPost(post)}>
                  Edit
                </button>
                <button onClick={() => deletePost(post.id)}>
                  Delete
                </button>
              </div>
            </div>
          ))}
        </div>
      )}
    </div>
  );
};

Custom Hooks for API Management

Basic API Hook

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true);
        setError(null);
       
        const response = await fetch(url, options);
       
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
       
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url]);

  const refetch = () => {
    fetchData();
  };

  return { data, loading, error, refetch };
};

// Usage
const UserProfile = ({ userId }) => {
  const { data: user, loading, error } = useApi(`/api/users/${userId}`);

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

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
};

Advanced API Hook with Cache

import { useState, useEffect, useRef } from 'react';

// Simple cache implementation
const cache = new Map();

const useApiWithCache = (url, options = {}) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const abortControllerRef = useRef();

  useEffect(() => {
    const fetchData = async () => {
      // Check cache first
      if (cache.has(url)) {
        setData(cache.get(url));
        setLoading(false);
        return;
      }

      try {
        setLoading(true);
        setError(null);

        // Create abort controller for request cancellation
        abortControllerRef.current = new AbortController();
       
        const response = await fetch(url, {
          ...options,
          signal: abortControllerRef.current.signal,
        });
       
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
       
        const result = await response.json();
       
        // Cache the result
        cache.set(url, result);
        setData(result);
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError(err.message);
        }
      } finally {
        setLoading(false);
      }
    };

    fetchData();

    // Cleanup function to abort request if component unmounts
    return () => {
      if (abortControllerRef.current) {
        abortControllerRef.current.abort();
      }
    };
  }, [url]);

  const clearCache = () => {
    cache.delete(url);
  };

  const refetch = () => {
    clearCache();
    // Trigger useEffect by updating a dependency
  };

  return { data, loading, error, refetch, clearCache };
};

CRUD Hook

import { useState, useCallback } from 'react';

const useCrud = (baseUrl) => {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const makeRequest = useCallback(async (url, options = {}) => {
    setLoading(true);
    setError(null);

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

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

      return await response.json();
    } catch (err) {
      setError(err.message);
      throw err;
    } finally {
      setLoading(false);
    }
  }, []);

  const create = useCallback(async (data) => {
    return makeRequest(baseUrl, {
      method: 'POST',
      body: JSON.stringify(data),
    });
  }, [baseUrl, makeRequest]);

  const read = useCallback(async (id) => {
    const url = id ? `${baseUrl}/${id}` : baseUrl;
    return makeRequest(url);
  }, [baseUrl, makeRequest]);

  const update = useCallback(async (id, data) => {
    return makeRequest(`${baseUrl}/${id}`, {
      method: 'PUT',
      body: JSON.stringify(data),
    });
  }, [baseUrl, makeRequest]);

  const remove = useCallback(async (id) => {
    return makeRequest(`${baseUrl}/${id}`, {
      method: 'DELETE',
    });
  }, [baseUrl, makeRequest]);

  return {
    create,
    read,
    update,
    remove,
    loading,
    error,
  };
};

// Usage
const TodoManager = () => {
  const [todos, setTodos] = useState([]);
  const { create, read, update, remove, loading, error } = useCrud('/api/todos');

  const loadTodos = async () => {
    try {
      const todosData = await read();
      setTodos(todosData);
    } catch (err) {
      console.error('Failed to load todos:', err);
    }
  };

  const addTodo = async (todoData) => {
    try {
      const newTodo = await create(todoData);
      setTodos(prev => [...prev, newTodo]);
    } catch (err) {
      console.error('Failed to create todo:', err);
    }
  };

  return (
    <div>
      {loading && <div>Loading...</div>}
      {error && <div>Error: {error}</div>}
      {/* Todo interface */}
    </div>
  );
};

Error Handling Strategies

Comprehensive Error Handler

import React, { useState } from 'react';

const ErrorBoundary = ({ children, fallback }) => {
  const [hasError, setHasError] = useState(false);

  if (hasError) {
    return fallback || <div>Something went wrong.</div>;
  }

  return children;
};

const ApiErrorHandler = {
  handle: (error, context = '') => {
    console.error(`API Error in ${context}:`, error);
   
    // Different handling based on error type
    if (error.name === 'TypeError') {
      return 'Network error. Please check your connection.';
    }
   
    if (error.message.includes('404')) {
      return 'Resource not found.';
    }
   
    if (error.message.includes('401')) {
      return 'Unauthorized. Please log in again.';
    }
   
    if (error.message.includes('403')) {
      return 'Access forbidden.';
    }
   
    if (error.message.includes('500')) {
      return 'Server error. Please try again later.';
    }
   
    return error.message || 'An unexpected error occurred.';
  }
};

const useApiWithErrorHandling = (url) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  const fetchData = async () => {
    try {
      setLoading(true);
      setError(null);
     
      const response = await fetch(url);
     
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }
     
      const result = await response.json();
      setData(result);
    } catch (err) {
      const errorMessage = ApiErrorHandler.handle(err, 'Data Fetch');
      setError(errorMessage);
    } finally {
      setLoading(false);
    }
  };

  React.useEffect(() => {
    fetchData();
  }, [url]);

  return { data, loading, error, refetch: fetchData };
};

Retry Logic

import { useState, useEffect } from 'react';

const useApiWithRetry = (url, maxRetries = 3, retryDelay = 1000) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [retryCount, setRetryCount] = useState(0);

  const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));

  const fetchWithRetry = async (attempt = 0) => {
    try {
      setLoading(true);
      setError(null);
     
      const response = await fetch(url);
     
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
     
      const result = await response.json();
      setData(result);
      setRetryCount(0);
    } catch (err) {
      if (attempt < maxRetries) {
        setRetryCount(attempt + 1);
        await delay(retryDelay * Math.pow(2, attempt)); // Exponential backoff
        return fetchWithRetry(attempt + 1);
      } else {
        setError(err.message);
      }
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchWithRetry();
  }, [url]);

  return { data, loading, error, retryCount };
};

Axios Integration

Basic Axios Setup

import axios from 'axios';

// Create axios instance with base configuration
const api = axios.create({
  baseURL: process.env.REACT_APP_API_BASE_URL || 'http://localhost:3001/api',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  },
});

// Request interceptor for adding auth tokens
api.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('authToken');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// Response interceptor for handling common errors
api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      // Handle unauthorized access
      localStorage.removeItem('authToken');
      window.location.href = '/login';
    }
   
    return Promise.reject(error);
  }
);

export default api;

Axios Hook

import { useState, useEffect } from 'react';
import api from './axiosConfig';

const useAxios = (config, dependencies = []) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const source = axios.CancelToken.source();
   
    const fetchData = async () => {
      try {
        setLoading(true);
        setError(null);
       
        const response = await api({
          ...config,
          cancelToken: source.token,
        });
       
        setData(response.data);
      } catch (err) {
        if (!axios.isCancel(err)) {
          setError(err.response?.data?.message || err.message);
        }
      } finally {
        setLoading(false);
      }
    };

    fetchData();

    return () => {
      source.cancel('Component unmounted');
    };
  }, dependencies);

  return { data, loading, error };
};

// Usage
const UserProfile = ({ userId }) => {
  const { data: user, loading, error } = useAxios({
    method: 'GET',
    url: `/users/${userId}`,
  }, [userId]);

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

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
};

React Query Integration

Basic React Query Setup

import { QueryClient, QueryClientProvider, useQuery, useMutation, useQueryClient } from 'react-query';

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

const App = () => {
  return (
    <QueryClientProvider client={queryClient}>
      <UserList />
    </QueryClientProvider>
  );
};

// API functions
const fetchUsers = async () => {
  const response = await fetch('/api/users');
  if (!response.ok) {
    throw new Error('Failed to fetch users');
  }
  return response.json();
};

const createUser = async (userData) => {
  const response = await fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(userData),
  });
 
  if (!response.ok) {
    throw new Error('Failed to create user');
  }
 
  return response.json();
};

const UserList = () => {
  const queryClient = useQueryClient();
 
  const {
    data: users,
    isLoading,
    error,
    refetch
  } = useQuery('users', fetchUsers);

  const createUserMutation = useMutation(createUser, {
    onSuccess: () => {
      // Invalidate and refetch users query
      queryClient.invalidateQueries('users');
    },
  });

  const handleCreateUser = (userData) => {
    createUserMutation.mutate(userData);
  };

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

  return (
    <div>
      <h2>Users</h2>
      <button onClick={() => handleCreateUser({ name: 'New User' })}>
        Add User
      </button>
      {users.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
};

API Authentication

JWT Token Management

import React, { createContext, useContext, useState, useEffect } from 'react';

const AuthContext = createContext();

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
};

export const AuthProvider = ({ children }) => {
  const [token, setToken] = useState(localStorage.getItem('authToken'));
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    if (token) {
      // Verify token and get user info
      fetchUserProfile();
    } else {
      setLoading(false);
    }
  }, [token]);

  const fetchUserProfile = async () => {
    try {
      const response = await fetch('/api/user/profile', {
        headers: {
          'Authorization': `Bearer ${token}`,
        },
      });
     
      if (response.ok) {
        const userData = await response.json();
        setUser(userData);
      } else {
        // Token is invalid
        logout();
      }
    } catch (error) {
      console.error('Failed to fetch user profile:', error);
      logout();
    } finally {
      setLoading(false);
    }
  };

  const login = async (credentials) => {
    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(credentials),
      });

      if (response.ok) {
        const { token: newToken, user: userData } = await response.json();
        setToken(newToken);
        setUser(userData);
        localStorage.setItem('authToken', newToken);
        return { success: true };
      } else {
        const error = await response.json();
        return { success: false, error: error.message };
      }
    } catch (error) {
      return { success: false, error: error.message };
    }
  };

  const logout = () => {
    setToken(null);
    setUser(null);
    localStorage.removeItem('authToken');
  };

  const value = {
    token,
    user,
    loading,
    login,
    logout,
    isAuthenticated: !!token && !!user,
  };

  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
};

// Authenticated API hook
const useAuthenticatedApi = () => {
  const { token } = useAuth();

  const makeAuthenticatedRequest = async (url, options = {}) => {
    const config = {
      ...options,
      headers: {
        'Content-Type': 'application/json',
        ...(token && { Authorization: `Bearer ${token}` }),
        ...options.headers,
      },
    };

    const response = await fetch(url, config);
   
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
   
    return response.json();
  };

  return { makeAuthenticatedRequest };
};

Performance Optimization

Request Deduplication

mport { useRef, useEffect, useState } from 'react';

// Cache for ongoing requests
const ongoingRequests = new Map();

const useDedupedApi = (url, options = {}) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const isMountedRef = useRef(true);

  useEffect(() => {
    const requestKey = `${url}-${JSON.stringify(options)}`;
   
    // Check if request is already ongoing
    if (ongoingRequests.has(requestKey)) {
      ongoingRequests.get(requestKey).then(result => {
        if (isMountedRef.current) {
          setData(result);
          setLoading(false);
        }
      }).catch(err => {
        if (isMountedRef.current) {
          setError(err.message);
          setLoading(false);
        }
      });
      return;
    }

    const fetchData = async () => {
      try {
        const response = await fetch(url, options);
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const result = await response.json();
        return result;
      } catch (err) {
        throw err;
      } finally {
        ongoingRequests.delete(requestKey);
      }
    };

    const requestPromise = fetchData();
    ongoingRequests.set(requestKey, requestPromise);

    requestPromise
      .then(result => {
        if (isMountedRef.current) {
          setData(result);
          setLoading(false);
        }
      })
      .catch(err => {
        if (isMountedRef.current) {
          setError(err.message);
          setLoading(false);
        }
      });

    return () => {
      isMountedRef.current = false;
    };
  }, [url, JSON.stringify(options)]);

  return { data, loading, error };
};

Pagination Hook

const usePagination = (fetchFunction, initialPage = 1, pageSize = 10) => {
  const [data, setData] = useState([]);
  const [currentPage, setCurrentPage] = useState(initialPage);
  const [totalPages, setTotalPages] = useState(0);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const fetchPage = async (page) => {
    setLoading(true);
    setError(null);
   
    try {
      const result = await fetchFunction(page, pageSize);
      setData(result.data);
      setTotalPages(result.totalPages);
      setCurrentPage(page);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchPage(currentPage);
  }, []);

  const goToPage = (page) => {
    if (page >= 1 && page <= totalPages) {
      fetchPage(page);
    }
  };

  const nextPage = () => {
    if (currentPage < totalPages) {
      goToPage(currentPage + 1);
    }
  };

  const prevPage = () => {
    if (currentPage > 1) {
      goToPage(currentPage - 1);
    }
  };

  return {
    data,
    loading,
    error,
    currentPage,
    totalPages,
    goToPage,
    nextPage,
    prevPage,
    hasNext: currentPage < totalPages,
    hasPrev: currentPage > 1,
  };
};

// Usage
const ProductList = () => {
  const fetchProducts = async (page, pageSize) => {
    const response = await fetch(`/api/products?page=${page}&limit=${pageSize}`);
    return response.json();
  };

  const {
    data: products,
    loading,
    error,
    currentPage,
    totalPages,
    nextPage,
    prevPage,
    hasNext,
    hasPrev,
  } = usePagination(fetchProducts);

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

  return (
    <div>
      <div className="products">
        {products.map(product => (
          <div key={product.id}>{product.name}</div>
        ))}
      </div>
     
      <div className="pagination">
        <button onClick={prevPage} disabled={!hasPrev}>
          Previous
        </button>
        <span>Page {currentPage} of {totalPages}</span>
        <button onClick={nextPage} disabled={!hasNext}>
          Next
        </button>
      </div>
    </div>
  );
};

Testing API Integration

Mocking API Calls

// __tests__/UserList.test.js
import { render, screen, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import UserList from '../UserList';

// Mock fetch
global.fetch = jest.fn();

const mockUsers = [
  { id: 1, name: 'John Doe', email: 'john@example.com' },
  { id: 2, name: 'Jane Smith', email: 'jane@example.com' },
];

beforeEach(() => {
  fetch.mockClear();
});

test('displays users after successful fetch', async () => {
  fetch.mockResolvedValueOnce({
    ok: true,
    json: async () => mockUsers,
  });

  render(<UserList />);

  expect(screen.getByText('Loading users...')).toBeInTheDocument();

  await waitFor(() => {
    expect(screen.getByText('John Doe - john@example.com')).toBeInTheDocument();
    expect(screen.getByText('Jane Smith - jane@example.com')).toBeInTheDocument();
  });

  expect(fetch).toHaveBeenCalledTimes(1);
});

test('displays error message on fetch failure', async () => {
  fetch.mockRejectedValueOnce(new Error('Network error'));

  render(<UserList />);

  await waitFor(() => {
    expect(screen.getByText('Error: Network error')).toBeInTheDocument();
  });
});

test('handles HTTP error responses', async () => {
  fetch.mockResolvedValueOnce({
    ok: false,
    status: 404,
  });

  render(<UserList />);

  await waitFor(() => {
    expect(screen.getByText(/Error: HTTP error! status: 404/)).toBeInTheDocument();
  });
});

Testing with Mock Service Worker (MSW)

// setupTests.js
import { setupServer } from 'msw/node';
import { rest } from 'msw';

const server = setupServer(
  rest.get('/api/users', (req, res, ctx) => {
    return res(
      ctx.json([
        { id: 1, name: 'John Doe', email: 'john@example.com' },
        { id: 2, name: 'Jane Smith', email: 'jane@example.com' },
      ])
    );
  }),

  rest.post('/api/users', (req, res, ctx) => {
    return res(
      ctx.json({ id: 3, name: 'New User', email: 'new@example.com' })
    );
  }),

  rest.delete('/api/users/:id', (req, res, ctx) => {
    return res(ctx.status(204));
  })
);

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

export { server };

Integration Test Example

import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import PostManager from '../PostManager';
import { server } from '../setupTests';
import { rest } from 'msw';

test('creates a new post successfully', async () => {
  const user = userEvent.setup();
 
  render(<PostManager />);

  // Wait for initial posts to load
  await waitFor(() => {
    expect(screen.getByText(/Post Manager/)).toBeInTheDocument();
  });

  // Fill out the form
  const titleInput = screen.getByPlaceholderText('Title');
  const bodyInput = screen.getByPlaceholderText('Body');
  const submitButton = screen.getByText('Create Post');

  await user.type(titleInput, 'Test Post');
  await user.type(bodyInput, 'This is a test post body');
  await user.click(submitButton);

  // Verify the new post appears
  await waitFor(() => {
    expect(screen.getByText('Test Post')).toBeInTheDocument();
    expect(screen.getByText('This is a test post body')).toBeInTheDocument();
  });
});

test('handles API error gracefully', async () => {
  // Override the handler to return an error
  server.use(
    rest.get('/api/posts', (req, res, ctx) => {
      return res(ctx.status(500));
    })
  );

  render(<PostManager />);

  await waitFor(() => {
    expect(screen.getByText(/Error/)).toBeInTheDocument();
  });
});

GraphQL Integration

Basic GraphQL Setup

import { ApolloClient, InMemoryCache, ApolloProvider, gql, useQuery, useMutation } from '@apollo/client';

const client = new ApolloClient({
  uri: 'http://localhost:4000/graphql',
  cache: new InMemoryCache(),
});

const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
      email
      posts {
        id
        title
      }
    }
  }
`;

const CREATE_USER = gql`
  mutation CreateUser($input: UserInput!) {
    createUser(input: $input) {
      id
      name
      email
    }
  }
`;

const UserListGraphQL = () => {
  const { loading, error, data, refetch } = useQuery(GET_USERS);
  const [createUser] = useMutation(CREATE_USER, {
    refetchQueries: [{ query: GET_USERS }],
  });

  const handleCreateUser = async () => {
    try {
      await createUser({
        variables: {
          input: {
            name: 'New User',
            email: 'new@example.com',
          },
        },
      });
    } catch (err) {
      console.error('Error creating user:', err);
    }
  };

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

  return (
    <div>
      <h2>Users (GraphQL)</h2>
      <button onClick={handleCreateUser}>Add User</button>
      <ul>
        {data.users.map(user => (
          <li key={user.id}>
            {user.name} - {user.email}
            <ul>
              {user.posts.map(post => (
                <li key={post.id}>{post.title}</li>
              ))}
            </ul>
          </li>
        ))}
      </ul>
    </div>
  );
};

const App = () => {
  return (
    <ApolloProvider client={client}>
      <UserListGraphQL />
    </ApolloProvider>
  );
};

API Configuration and Environment Management

Environment-based API Configuration

// config/api.js
const API_CONFIG = {
  development: {
    baseURL: 'http://localhost:3001/api',
    timeout: 10000,
    retries: 3,
  },
  staging: {
    baseURL: 'https://staging-api.example.com/api',
    timeout: 15000,
    retries: 2,
  },
  production: {
    baseURL: 'https://api.example.com/api',
    timeout: 20000,
    retries: 1,
  },
};

const getApiConfig = () => {
  const env = process.env.NODE_ENV || 'development';
  return API_CONFIG[env];
};

export default getApiConfig();

API Service Class

/ services/ApiService.js
import apiConfig from '../config/api';

class ApiService {
  constructor() {
    this.baseURL = apiConfig.baseURL;
    this.timeout = apiConfig.timeout;
    this.defaultHeaders = {
      'Content-Type': 'application/json',
    };
  }

  setAuthToken(token) {
    this.defaultHeaders.Authorization = `Bearer ${token}`;
  }

  removeAuthToken() {
    delete this.defaultHeaders.Authorization;
  }

  async request(endpoint, options = {}) {
    const url = `${this.baseURL}${endpoint}`;
    const config = {
      headers: {
        ...this.defaultHeaders,
        ...options.headers,
      },
      ...options,
    };

    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), this.timeout);

    try {
      const response = await fetch(url, {
        ...config,
        signal: controller.signal,
      });

      clearTimeout(timeoutId);

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

      return await response.json();
    } catch (error) {
      clearTimeout(timeoutId);
     
      if (error.name === 'AbortError') {
        throw new Error('Request timeout');
      }
     
      throw error;
    }
  }

  // CRUD methods
  async get(endpoint, params = {}) {
    const queryString = new URLSearchParams(params).toString();
    const url = queryString ? `${endpoint}?${queryString}` : endpoint;
    return this.request(url);
  }

  async post(endpoint, data) {
    return this.request(endpoint, {
      method: 'POST',
      body: JSON.stringify(data),
    });
  }

  async put(endpoint, data) {
    return this.request(endpoint, {
      method: 'PUT',
      body: JSON.stringify(data),
    });
  }

  async patch(endpoint, data) {
    return this.request(endpoint, {
      method: 'PATCH',
      body: JSON.stringify(data),
    });
  }

  async delete(endpoint) {
    return this.request(endpoint, {
      method: 'DELETE',
    });
  }
}

export default new ApiService();

Real-World API Integration Example

Complete User Management System

import React, { useState, useEffect } from 'react';
import ApiService from '../services/ApiService';

const UserManagementSystem = () => {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [selectedUser, setSelectedUser] = useState(null);
  const [isEditing, setIsEditing] = useState(false);
  const [searchTerm, setSearchTerm] = useState('');
  const [currentPage, setCurrentPage] = useState(1);
  const [totalPages, setTotalPages] = useState(1);

  const [formData, setFormData] = useState({
    name: '',
    email: '',
    role: 'user',
  });

  // Load users with pagination and search
  const loadUsers = async (page = 1, search = '') => {
    try {
      setLoading(true);
      setError(null);
     
      const params = {
        page,
        limit: 10,
        ...(search && { search }),
      };
     
      const response = await ApiService.get('/users', params);
     
      setUsers(response.data);
      setCurrentPage(response.currentPage);
      setTotalPages(response.totalPages);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  // Create user
  const createUser = async (userData) => {
    try {
      setLoading(true);
      const newUser = await ApiService.post('/users', userData);
      setUsers(prev => [newUser, ...prev]);
      setFormData({ name: '', email: '', role: 'user' });
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  // Update user
  const updateUser = async (id, userData) => {
    try {
      setLoading(true);
      const updatedUser = await ApiService.put(`/users/${id}`, userData);
      setUsers(prev => prev.map(user =>
        user.id === id ? updatedUser : user
      ));
      setSelectedUser(null);
      setIsEditing(false);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  // Delete user
  const deleteUser = async (id) => {
    if (!window.confirm('Are you sure you want to delete this user?')) {
      return;
    }

    try {
      setLoading(true);
      await ApiService.delete(`/users/${id}`);
      setUsers(prev => prev.filter(user => user.id !== id));
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  // Handle form submission
  const handleSubmit = (e) => {
    e.preventDefault();
   
    if (isEditing && selectedUser) {
      updateUser(selectedUser.id, formData);
    } else {
      createUser(formData);
    }
  };

  // Handle search
  const handleSearch = (e) => {
    e.preventDefault();
    loadUsers(1, searchTerm);
  };

  // Load initial data
  useEffect(() => {
    loadUsers();
  }, []);

  // Handle edit user
  const handleEditUser = (user) => {
    setSelectedUser(user);
    setFormData({
      name: user.name,
      email: user.email,
      role: user.role,
    });
    setIsEditing(true);
  };

  // Cancel editing
  const handleCancelEdit = () => {
    setSelectedUser(null);
    setIsEditing(false);
    setFormData({ name: '', email: '', role: 'user' });
  };

  return (
    <div className="user-management">
      <h1>User Management System</h1>

      {error && (
        <div className="error-message">
          Error: {error}
          <button onClick={() => setError(null)}>×</button>
        </div>
      )}

      {/* Search Form */}
      <form onSubmit={handleSearch} className="search-form">
        <input
          type="text"
          placeholder="Search users..."
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
        />
        <button type="submit">Search</button>
        <button
          type="button"
          onClick={() => {
            setSearchTerm('');
            loadUsers();
          }}
        >
          Clear
        </button>
      </form>

      {/* User Form */}
      <form onSubmit={handleSubmit} className="user-form">
        <h3>{isEditing ? 'Edit User' : 'Create New User'}</h3>
       
        <input
          type="text"
          placeholder="Name"
          value={formData.name}
          onChange={(e) => setFormData({...formData, name: e.target.value})}
          required
        />
       
        <input
          type="email"
          placeholder="Email"
          value={formData.email}
          onChange={(e) => setFormData({...formData, email: e.target.value})}
          required
        />
       
        <select
          value={formData.role}
          onChange={(e) => setFormData({...formData, role: e.target.value})}
        >
          <option value="user">User</option>
          <option value="admin">Admin</option>
          <option value="moderator">Moderator</option>
        </select>
       
        <div className="form-actions">
          <button type="submit" disabled={loading}>
            {loading ? 'Processing...' : (isEditing ? 'Update User' : 'Create User')}
          </button>
          {isEditing && (
            <button type="button" onClick={handleCancelEdit}>
              Cancel
            </button>
          )}
        </div>
      </form>

      {/* Users List */}
      <div className="users-list">
        <h3>Users ({users.length})</h3>
       
        {loading && <div className="loading">Loading users...</div>}
       
        {users.length === 0 && !loading && (
          <div className="no-users">No users found.</div>
        )}
       
        <div className="users-grid">
          {users.map(user => (
            <div key={user.id} className="user-card">
              <div className="user-info">
                <h4>{user.name}</h4>
                <p>{user.email}</p>
                <span className={`role role-${user.role}`}>{user.role}</span>
              </div>
             
              <div className="user-actions">
                <button onClick={() => handleEditUser(user)}>
                  Edit
                </button>
                <button
                  onClick={() => deleteUser(user.id)}
                  className="delete-btn"
                  disabled={loading}
                >
                  Delete
                </button>
              </div>
            </div>
          ))}
        </div>

        {/* Pagination */}
        {totalPages > 1 && (
          <div className="pagination">
            <button
              onClick={() => loadUsers(currentPage - 1, searchTerm)}
              disabled={currentPage === 1 || loading}
            >
              Previous
            </button>
           
            <span>
              Page {currentPage} of {totalPages}
            </span>
           
            <button
              onClick={() => loadUsers(currentPage + 1, searchTerm)}
              disabled={currentPage === totalPages || loading}
            >
              Next
            </button>
          </div>
        )}
      </div>
    </div>
  );
};

export default UserManagementSystem;

Best Practices

  1. Error Handling: Always implement comprehensive error handling for all API calls
  2. Loading States: Provide clear loading indicators for better user experience
  3. Caching: Implement appropriate caching strategies to reduce unnecessary requests
  4. Request Cancellation: Cancel ongoing requests when components unmount
  5. Retry Logic: Implement retry mechanisms for failed requests
  6. Authentication: Secure API calls with proper authentication tokens
  7. Validation: Validate data both on client and server sides
  8. Performance: Use pagination, debouncing, and request deduplication
  9. Testing: Write comprehensive tests for API integration
  10. Environment Configuration: Use different API configurations for different environments

Summary

Working with APIs in React involves several key concepts and patterns:

  • Basic fetch operations for simple CRUD functionality
  • Custom hooks for reusable API logic
  • Error handling and retry mechanisms for robust applications
  • Authentication and security considerations
  • Performance optimization through caching and request management
  • Testing strategies for reliable API integration
  • Advanced libraries like Axios, React Query, and Apollo Client for enhanced functionality

The key to successful API integration is building reusable, maintainable, and robust solutions that handle edge cases gracefully while providing excellent user experience. Start with simple implementations and gradually add complexity based on your application’s needs.

Related Articles

Scroll to Top