API Requests with Axios

Mastering HTTP requests in React applications

Introduction to Axios

Axios is a promise-based HTTP client for the browser and Node.js that makes it easy to send asynchronous HTTP requests and handle responses. It has become the de facto standard for making API requests in React applications due to its simplicity, powerful features, and cross-browser compatibility.

Key Features of Axios

Axios vs. Fetch API

Feature Axios Fetch API
Browser Support All browsers via polyfills Modern browsers only
Response Timeout Yes, built-in Requires manual implementation
Request Cancellation Yes, built-in Requires AbortController
Automatic JSON Parsing Yes Requires .json() call
Error Handling HTTP errors trigger catch blocks HTTP error responses still resolve
Interceptors Yes, built-in Requires manual implementation
Request/Response Transform Yes, built-in Requires manual implementation
Download Progress Yes No built-in support

Real-world analogy: If HTTP requests were like sending mail, Fetch would be like basic postal service (gets the job done with minimal features), while Axios would be like a premium courier service with tracking, insurance, and special handling options.

Getting Started with Axios

Installation

Install Axios in your React project using npm or yarn:

npm install axios
# or
yarn add axios

Basic Usage

Here's a simple example of how to use Axios for different types of HTTP requests:

import axios from 'axios';

// GET request
const fetchUsers = async () => {
  try {
    const response = await axios.get('https://api.example.com/users');
    console.log(response.data);
    return response.data;
  } catch (error) {
    console.error('Error fetching users:', error);
    throw error;
  }
};

// POST request
const createUser = async (userData) => {
  try {
    const response = await axios.post('https://api.example.com/users', userData);
    console.log('User created:', response.data);
    return response.data;
  } catch (error) {
    console.error('Error creating user:', error);
    throw error;
  }
};

// PUT request
const updateUser = async (userId, userData) => {
  try {
    const response = await axios.put(`https://api.example.com/users/${userId}`, userData);
    console.log('User updated:', response.data);
    return response.data;
  } catch (error) {
    console.error('Error updating user:', error);
    throw error;
  }
};

// DELETE request
const deleteUser = async (userId) => {
  try {
    const response = await axios.delete(`https://api.example.com/users/${userId}`);
    console.log('User deleted:', response.data);
    return response.data;
  } catch (error) {
    console.error('Error deleting user:', error);
    throw error;
  }
};

Axios Response Object

When an Axios request is successful, it returns a response object with the following properties:

{
  // `data` is the response that was provided by the server
  data: {},

  // `status` is the HTTP status code from the server response
  status: 200,

  // `statusText` is the HTTP status message from the server response
  statusText: 'OK',

  // `headers` the HTTP headers that the server responded with
  headers: {},

  // `config` is the config that was provided to `axios` for the request
  config: {},

  // `request` is the request that generated this response
  request: {}
}

Error Handling

When an Axios request fails, it returns an error object that contains information about the error:

axios.get('/api/data')
  .then(response => {
    // Handle success
    console.log(response.data);
  })
  .catch(error => {
    if (error.response) {
      // The request was made and the server responded with a status code
      // that falls out of the range of 2xx
      console.error('Error data:', error.response.data);
      console.error('Error status:', error.response.status);
      console.error('Error headers:', error.response.headers);
    } else if (error.request) {
      // The request was made but no response was received
      console.error('No response received:', error.request);
    } else {
      // Something happened in setting up the request that triggered an Error
      console.error('Error message:', error.message);
    }
    console.error('Error config:', error.config);
  });
flowchart TD A[Axios Request] --> B{Success?} B -->|Yes| C[Response with data] B -->|No| D{Response Received?} D -->|Yes| E[Error with response] D -->|No| F[Error with request] style A fill:#d4f0f0,stroke:#000 style B fill:#ffeecc,stroke:#000 style C fill:#d4f0f0,stroke:#000 style D fill:#ffeecc,stroke:#000 style E fill:#ffdddd,stroke:#000 style F fill:#ffdddd,stroke:#000

Creating an Axios Instance

Instead of using Axios methods directly, it's a best practice to create a custom Axios instance with predefined configurations. This approach makes your code more maintainable and allows for consistent handling of requests across your application.

// services/api.js
import axios from 'axios';

// Create an instance with custom configs
const api = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
    'Accept': 'application/json'
  }
});

export default api;

Using environment variables for different environments:

// services/api.js
import axios from 'axios';

const baseURL = process.env.REACT_APP_API_URL || 'https://api.example.com';

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

export default api;

Real-world application: This approach is used in production applications to manage different API endpoints for development, staging, and production environments.

Using the Axios Instance

// services/userService.js
import api from './api';

export const userService = {
  // Get all users
  getUsers: async () => {
    const response = await api.get('/users');
    return response.data;
  },
  
  // Get user by ID
  getUserById: async (id) => {
    const response = await api.get(`/users/${id}`);
    return response.data;
  },
  
  // Create a new user
  createUser: async (userData) => {
    const response = await api.post('/users', userData);
    return response.data;
  },
  
  // Update a user
  updateUser: async (id, userData) => {
    const response = await api.put(`/users/${id}`, userData);
    return response.data;
  },
  
  // Delete a user
  deleteUser: async (id) => {
    const response = await api.delete(`/users/${id}`);
    return response.data;
  }
};

Advantages of this approach:

Request and Response Interceptors

Interceptors are one of the most powerful features of Axios. They allow you to intercept requests or responses before they are handled by then or catch.

Request Interceptors

Request interceptors can be used to add authentication tokens, format request data, or log requests. This is particularly useful for adding JWT tokens to every outgoing request.

// Add a request interceptor
api.interceptors.request.use(
  (config) => {
    // Do something before request is sent
    const token = localStorage.getItem('token');
    
    if (token) {
      config.headers['Authorization'] = `Bearer ${token}`;
    }
    
    return config;
  },
  (error) => {
    // Do something with request error
    return Promise.reject(error);
  }
);

Response Interceptors

Response interceptors can be used to globally process response data, handle errors, or refresh tokens when they expire.

// Add a response interceptor
api.interceptors.response.use(
  (response) => {
    // Any status code within the range of 2xx causes this function to trigger
    // Do something with response data
    return response;
  },
  async (error) => {
    const originalRequest = error.config;
    
    // If error is 401 (Unauthorized) and not already retrying
    if (error.response.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      
      try {
        // Attempt to refresh the token
        const refreshToken = localStorage.getItem('refreshToken');
        const response = await api.post('/auth/refresh', { refreshToken });
        
        // Store the new tokens
        const { token } = response.data;
        localStorage.setItem('token', token);
        
        // Update the authorization header
        api.defaults.headers.common['Authorization'] = `Bearer ${token}`;
        
        // Retry the original request with the new token
        return api(originalRequest);
      } catch (refreshError) {
        // If refresh fails, redirect to login
        localStorage.removeItem('token');
        localStorage.removeItem('refreshToken');
        window.location.href = '/login';
        return Promise.reject(refreshError);
      }
    }
    
    // Return any other error
    return Promise.reject(error);
  }
);
sequenceDiagram participant App participant RequestInterceptor participant API participant ResponseInterceptor App->>RequestInterceptor: Make API Request RequestInterceptor->>RequestInterceptor: Add Auth Token RequestInterceptor->>API: Modified Request API-->>ResponseInterceptor: Response alt Response OK (2xx) ResponseInterceptor-->>App: Processed Response else Unauthorized (401) ResponseInterceptor->>ResponseInterceptor: Try to refresh token alt Token Refresh Successful ResponseInterceptor->>API: Retry Original Request API-->>ResponseInterceptor: New Response ResponseInterceptor-->>App: Processed Response else Token Refresh Failed ResponseInterceptor-->>App: Redirect to Login end else Other Error ResponseInterceptor-->>App: Error Response end

Real-world application: This pattern is commonly used in production applications to handle authentication token expiration without disrupting the user experience. When a token expires, the app automatically refreshes it and retries the failed request.

Advanced Axios Features

Concurrent Requests

You can use axios.all() to make multiple requests in parallel and handle all the responses together.

// Make multiple concurrent requests
const fetchDashboardData = async () => {
  try {
    // Define all requests
    const userRequest = api.get('/users/me');
    const postsRequest = api.get('/posts');
    const notificationsRequest = api.get('/notifications');
    
    // Execute all requests concurrently
    const [userResponse, postsResponse, notificationsResponse] = await Promise.all([
      userRequest,
      postsRequest,
      notificationsRequest
    ]);
    
    // Process all responses together
    return {
      user: userResponse.data,
      posts: postsResponse.data,
      notifications: notificationsResponse.data
    };
  } catch (error) {
    console.error('Error fetching dashboard data:', error);
    throw error;
  }
};

Request Cancellation

Axios supports cancelling requests using the CancelToken API. This is useful for scenarios where you need to cancel an ongoing request, such as when a user navigates away from a page or types in a search field.

const CancelToken = axios.CancelToken;
let cancel;

// Search function with cancellation
const searchUsers = async (query) => {
  try {
    // Cancel the previous request if it exists
    if (cancel) {
      cancel('New search request initiated');
    }
    
    // Make a new request with a cancel token
    const response = await api.get('/users/search', {
      params: { q: query },
      cancelToken: new CancelToken(function executor(c) {
        // An executor function receives a cancel function as a parameter
        cancel = c;
      })
    });
    
    return response.data;
  } catch (error) {
    if (axios.isCancel(error)) {
      // Request was cancelled
      console.log('Request cancelled:', error.message);
      return [];
    }
    
    // Handle other errors
    console.error('Search error:', error);
    throw error;
  }
};

In React components, it's common to use this pattern with the useEffect hook for cleanup:

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

function SearchComponent() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    // Skip if query is empty
    if (!query.trim()) {
      setResults([]);
      return;
    }
    
    // Create a cancel token
    const CancelToken = axios.CancelToken;
    const source = CancelToken.source();
    
    const fetchResults = async () => {
      setLoading(true);
      setError(null);
      
      try {
        const response = await api.get('/search', {
          params: { q: query },
          cancelToken: source.token
        });
        
        setResults(response.data);
      } catch (err) {
        if (axios.isCancel(err)) {
          // Request was cancelled, no need to set error
          console.log('Request cancelled:', err.message);
        } else {
          setError('Failed to fetch results');
          console.error(err);
        }
      } finally {
        setLoading(false);
      }
    };
    
    // Debounce the search to avoid making too many requests
    const timeoutId = setTimeout(() => {
      fetchResults();
    }, 500);
    
    // Cleanup function
    return () => {
      clearTimeout(timeoutId);
      source.cancel('Component unmounted or new search initiated');
    };
  }, [query]);
  
  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      
      {loading && <div>Loading...</div>}
      {error && <div className="error">{error}</div>}
      
      <ul>
        {results.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

Timeout Configuration

You can set timeouts for requests to prevent them from hanging indefinitely.

// Global timeout for all requests
const api = axios.create({
  baseURL: '/api',
  timeout: 10000, // 10 seconds
});

// Timeout for a specific request
api.get('/users', {
  timeout: 5000 // 5 seconds
});

File Upload with Progress Tracking

Axios makes it easy to track upload progress for file uploads, which is useful for providing feedback to users.

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

function FileUploader() {
  const [file, setFile] = useState(null);
  const [progress, setProgress] = useState(0);
  const [uploading, setUploading] = useState(false);
  const [error, setError] = useState(null);
  const [success, setSuccess] = useState(false);
  
  const handleFileChange = (e) => {
    if (e.target.files[0]) {
      setFile(e.target.files[0]);
      setProgress(0);
      setError(null);
      setSuccess(false);
    }
  };
  
  const handleUpload = async () => {
    if (!file) {
      setError('Please select a file');
      return;
    }
    
    setUploading(true);
    setProgress(0);
    setError(null);
    setSuccess(false);
    
    // Create form data
    const formData = new FormData();
    formData.append('file', file);
    
    try {
      await axios.post('/api/upload', formData, {
        headers: {
          'Content-Type': 'multipart/form-data'
        },
        onUploadProgress: (progressEvent) => {
          const percentCompleted = Math.round(
            (progressEvent.loaded * 100) / progressEvent.total
          );
          setProgress(percentCompleted);
        }
      });
      
      setSuccess(true);
    } catch (err) {
      setError('Upload failed: ' + (err.response?.data?.message || err.message));
    } finally {
      setUploading(false);
    }
  };
  
  return (
    <div className="file-uploader">
      <input 
        type="file" 
        onChange={handleFileChange} 
        disabled={uploading}
      />
      
      {file && (
        <div>
          <p>Selected file: {file.name}</p>
          <button 
            onClick={handleUpload} 
            disabled={uploading}
          >
            {uploading ? 'Uploading...' : 'Upload'}
          </button>
        </div>
      )}
      
      {uploading && (
        <div className="progress-bar-container">
          <div 
            className="progress-bar" 
            style={{ width: `${progress}%` }}
          ></div>
          <span>{progress}%</span>
        </div>
      )}
      
      {error && <div className="error">{error}</div>}
      {success && <div className="success">File uploaded successfully!</div>}
    </div>
  );
}

Using Axios with React Hooks

Creating custom hooks for API calls can greatly simplify your React components and provide reusable data fetching logic.

useApi Custom Hook

// hooks/useApi.js
import { useState, useEffect, useCallback } from 'react';
import axios from 'axios';

const useApi = (url, options = {}) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [controller, setController] = useState(null);
  
  // Function to fetch data
  const fetchData = useCallback(async (body = null) => {
    // Cancel previous request
    if (controller) {
      controller.abort();
    }
    
    // Create a new AbortController
    const newController = new AbortController();
    setController(newController);
    
    setLoading(true);
    setError(null);
    
    try {
      const response = await axios({
        url,
        ...options,
        signal: newController.signal,
        data: body
      });
      
      setData(response.data);
      return response.data;
    } catch (err) {
      if (axios.isCancel(err)) {
        // Request was cancelled
        console.log('Request cancelled:', err.message);
      } else {
        setError(err.response?.data?.message || err.message);
        throw err;
      }
    } finally {
      setLoading(false);
    }
  }, [url, options]);
  
  // Initial data fetch if autoFetch is true
  useEffect(() => {
    if (options.autoFetch !== false) {
      fetchData();
    }
    
    // Cleanup: cancel request on unmount
    return () => {
      if (controller) {
        controller.abort();
      }
    };
  }, [fetchData, options.autoFetch]);
  
  return { data, loading, error, fetchData };
};

export default useApi;

Using the Custom Hook

import React from 'react';
import useApi from '../hooks/useApi';

function UserList() {
  const { data: users, loading, error, fetchData: refetch } = useApi('/api/users');
  
  const handleRefresh = () => {
    refetch();
  };
  
  if (loading) {
    return <div>Loading users...</div>;
  }
  
  if (error) {
    return (
      <div>
        <p>Error: {error}</p>
        <button onClick={handleRefresh}>Try Again</button>
      </div>
    );
  }
  
  return (
    <div>
      <h2>Users</h2>
      <button onClick={handleRefresh}>Refresh</button>
      
      {users && users.length > 0 ? (
        <ul>
          {users.map(user => (
            <li key={user.id}>{user.name} ({user.email})</li>
          ))}
        </ul>
      ) : (
        <p>No users found.</p>
      )}
    </div>
  );
}

CRUD Operations Hook

// hooks/useCrud.js
import { useState } from 'react';
import api from '../services/api';

const useCrud = (baseUrl) => {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  // Get all items
  const getAll = async () => {
    setLoading(true);
    setError(null);
    
    try {
      const response = await api.get(baseUrl);
      setData(response.data);
      return response.data;
    } catch (err) {
      setError(err.response?.data?.message || err.message);
      throw err;
    } finally {
      setLoading(false);
    }
  };
  
  // Get single item by ID
  const getById = async (id) => {
    setLoading(true);
    setError(null);
    
    try {
      const response = await api.get(`${baseUrl}/${id}`);
      return response.data;
    } catch (err) {
      setError(err.response?.data?.message || err.message);
      throw err;
    } finally {
      setLoading(false);
    }
  };
  
  // Create new item
  const create = async (item) => {
    setLoading(true);
    setError(null);
    
    try {
      const response = await api.post(baseUrl, item);
      setData(prevData => [...prevData, response.data]);
      return response.data;
    } catch (err) {
      setError(err.response?.data?.message || err.message);
      throw err;
    } finally {
      setLoading(false);
    }
  };
  
  // Update existing item
  const update = async (id, item) => {
    setLoading(true);
    setError(null);
    
    try {
      const response = await api.put(`${baseUrl}/${id}`, item);
      setData(prevData => 
        prevData.map(d => d.id === id ? response.data : d)
      );
      return response.data;
    } catch (err) {
      setError(err.response?.data?.message || err.message);
      throw err;
    } finally {
      setLoading(false);
    }
  };
  
  // Delete item
  const remove = async (id) => {
    setLoading(true);
    setError(null);
    
    try {
      await api.delete(`${baseUrl}/${id}`);
      setData(prevData => prevData.filter(d => d.id !== id));
      return true;
    } catch (err) {
      setError(err.response?.data?.message || err.message);
      throw err;
    } finally {
      setLoading(false);
    }
  };
  
  return {
    data,
    loading,
    error,
    getAll,
    getById,
    create,
    update,
    remove
  };
};

export default useCrud;

Using the CRUD Hook in a Component

import React, { useState, useEffect } from 'react';
import useCrud from '../hooks/useCrud';

function TodoManager() {
  const {
    data: todos,
    loading,
    error,
    getAll,
    create,
    update,
    remove
  } = useCrud('/api/todos');
  
  const [newTodo, setNewTodo] = useState('');
  
  useEffect(() => {
    // Load todos when component mounts
    getAll();
  }, [getAll]);
  
  const handleAddTodo = async (e) => {
    e.preventDefault();
    
    if (!newTodo.trim()) return;
    
    try {
      await create({ title: newTodo, completed: false });
      setNewTodo('');
    } catch (err) {
      console.error('Failed to add todo:', err);
    }
  };
  
  const handleToggleComplete = async (id, completed) => {
    try {
      await update(id, { completed: !completed });
    } catch (err) {
      console.error('Failed to update todo:', err);
    }
  };
  
  const handleDelete = async (id) => {
    try {
      await remove(id);
    } catch (err) {
      console.error('Failed to delete todo:', err);
    }
  };
  
  if (loading && !todos.length) {
    return <div>Loading todos...</div>;
  }
  
  if (error) {
    return <div>Error: {error}</div>;
  }
  
  return (
    <div>
      <h2>Todo List</h2>
      
      <form onSubmit={handleAddTodo}>
        <input
          type="text"
          value={newTodo}
          onChange={(e) => setNewTodo(e.target.value)}
          placeholder="Add a new todo"
          disabled={loading}
        />
        <button type="submit" disabled={loading || !newTodo.trim()}>
          {loading ? 'Adding...' : 'Add'}
        </button>
      </form>
      
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => handleToggleComplete(todo.id, todo.completed)}
            />
            <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
              {todo.title}
            </span>
            <button onClick={() => handleDelete(todo.id)}>Delete</button>
          </li>
        ))}
      </ul>
      
      {!todos.length && !loading && (
        <p>No todos yet. Add one to get started!</p>
      )}
    </div>
  );
}

Testing Axios API Calls

Testing API calls is crucial for ensuring the reliability of your application. There are several approaches to testing Axios requests:

Mock Axios for Unit Tests

For unit tests, you typically want to mock Axios to avoid making real API calls. The `jest-mock-axios` or `axios-mock-adapter` libraries can help with this.

// Example using axios-mock-adapter
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { userService } from './userService';

describe('User Service', () => {
  let mock;
  
  beforeEach(() => {
    // Create a new instance of mock adapter
    mock = new MockAdapter(axios);
  });
  
  afterEach(() => {
    // Reset the mock
    mock.reset();
  });
  
  test('getUsers should return a list of users', async () => {
    const users = [
      { id: 1, name: 'John Doe' },
      { id: 2, name: 'Jane Smith' }
    ];
    
    // Setup the mock to return the users
    mock.onGet('/api/users').reply(200, users);
    
    // Call the service method
    const result = await userService.getUsers();
    
    // Assert the result
    expect(result).toEqual(users);
  });
  
  test('getUsers should handle errors', async () => {
    // Setup the mock to return an error
    mock.onGet('/api/users').reply(500, { message: 'Server error' });
    
    // Call the service method and expect it to throw
    await expect(userService.getUsers()).rejects.toThrow();
  });
  
  test('createUser should post user data', async () => {
    const newUser = { name: 'New User', email: 'new@example.com' };
    const createdUser = { id: 3, ...newUser };
    
    // Setup the mock
    mock.onPost('/api/users', newUser).reply(201, createdUser);
    
    // Call the service method
    const result = await userService.createUser(newUser);
    
    // Assert the result
    expect(result).toEqual(createdUser);
  });
});

Testing Hooks with Axios

// Example testing a custom hook with React Testing Library
import { renderHook, act } from '@testing-library/react-hooks';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import useApi from './useApi';

describe('useApi hook', () => {
  let mock;
  
  beforeEach(() => {
    mock = new MockAdapter(axios);
  });
  
  afterEach(() => {
    mock.reset();
  });
  
  test('should fetch data successfully', async () => {
    const data = { id: 1, name: 'Test Item' };
    mock.onGet('/api/test').reply(200, data);
    
    const { result, waitForNextUpdate } = renderHook(() => 
      useApi('/api/test')
    );
    
    // Initially loading is true, and data is null
    expect(result.current.loading).toBe(true);
    expect(result.current.data).toBe(null);
    
    // Wait for the hook to update
    await waitForNextUpdate();
    
    // After the update, loading should be false and data should be set
    expect(result.current.loading).toBe(false);
    expect(result.current.data).toEqual(data);
    expect(result.current.error).toBe(null);
  });
  
  test('should handle error', async () => {
    mock.onGet('/api/test').reply(500, { message: 'Server error' });
    
    const { result, waitForNextUpdate } = renderHook(() => 
      useApi('/api/test')
    );
    
    await waitForNextUpdate();
    
    expect(result.current.loading).toBe(false);
    expect(result.current.data).toBe(null);
    expect(result.current.error).toBeTruthy();
  });
  
  test('should refetch data when called explicitly', async () => {
    const initialData = { id: 1, name: 'Initial' };
    const updatedData = { id: 1, name: 'Updated' };
    
    // First reply with initialData
    mock.onGet('/api/test').replyOnce(200, initialData);
    
    const { result, waitForNextUpdate } = renderHook(() => 
      useApi('/api/test')
    );
    
    await waitForNextUpdate();
    
    expect(result.current.data).toEqual(initialData);
    
    // Update mock to return new data
    mock.onGet('/api/test').replyOnce(200, updatedData);
    
    // Call fetchData manually
    act(() => {
      result.current.fetchData();
    });
    
    // Wait for the update
    await waitForNextUpdate();
    
    expect(result.current.data).toEqual(updatedData);
  });
});

Real-world application: These testing patterns are used in production applications to ensure API calls work correctly across the application, especially when refactoring code or upgrading dependencies.

Best Practices for Axios in React Applications

graph TD A[React Component] --> B[Custom Hook] B --> C[API Service] C --> D[Axios Instance] D --> E{Interceptors} E --> F[Request] F --> G[Server] G --> H[Response] H --> E E --> I[Processed Response] I --> C C --> B B --> A style A fill:#d4f0f0,stroke:#000 style B fill:#d4f0f0,stroke:#000 style C fill:#ffe6cc,stroke:#000 style D fill:#ffe6cc,stroke:#000 style E fill:#ffe6cc,stroke:#000 style F fill:#ffe6cc,stroke:#000 style G fill:#e6ccff,stroke:#000 style H fill:#ffe6cc,stroke:#000 style I fill:#ffe6cc,stroke:#000

Example Project Structure

src/
|-- api/
|   |-- axios.js           # Axios instance and interceptors
|   |-- index.js           # Re-exports all services
|   |-- userService.js     # User-related API calls
|   |-- postService.js     # Post-related API calls
|   `-- ...
|
|-- hooks/
|   |-- useApi.js          # Generic API hook
|   |-- useUser.js         # User-specific hook
|   |-- usePost.js         # Post-specific hook
|   `-- ...
|
|-- components/
|   |-- users/
|   |   |-- UserList.js
|   |   |-- UserDetail.js
|   |   `-- ...
|   |
|   |-- posts/
|   |   |-- PostList.js
|   |   |-- PostDetail.js
|   |   `-- ...
|   `-- ...
|
|-- App.js
`-- index.js

Practice Activities

Activity 1: Basic CRUD Application

Create a simple CRUD application for managing a list of products:

  • Set up an Axios instance with baseURL and appropriate headers
  • Create service functions for all CRUD operations
  • Build React components to display, add, edit, and delete products
  • Implement proper loading and error states

Use json-server as a mock backend to test your application.

Activity 2: Authentication System

Build an authentication system using Axios:

  • Implement login and registration forms
  • Create Axios interceptors to handle JWT tokens
  • Implement token refresh logic when tokens expire
  • Add protected routes that require authentication

Activity 3: File Upload with Progress

Create a file upload component:

  • Build a file upload form that supports single and multiple files
  • Implement progress tracking and display using Axios
  • Add validation for file types and sizes
  • Create a preview component for uploaded images

Activity 4: API Testing

Write tests for your API service:

  • Set up Jest and Axios mock adapter
  • Write unit tests for all API service functions
  • Test error scenarios and edge cases
  • Test any custom hooks that use Axios

Summary

Mastering Axios is essential for building robust React applications that communicate effectively with backend services. By implementing the patterns and practices covered in this lecture, you'll be able to create maintainable, testable, and user-friendly applications that provide excellent experiences even when dealing with network requests.

Further Resources