Ora

How do I add Axios interceptor?

Published in Axios Interceptors 8 mins read

Adding Axios interceptors allows you to globally handle HTTP requests and responses before they are sent or after they are received, centralizing logic for tasks like authentication, logging, or error handling. This powerful feature enables you to modify requests and responses with ease across your entire application.

Axios, a popular promise-based HTTP client, provides two main types of interceptors:

  • Request Interceptors: These are executed before an HTTP request is sent to the server.
  • Response Interceptors: These are executed after an HTTP response is received from the server but before it's processed by your application's then or catch blocks.

Step-by-Step Guide to Adding Axios Interceptors

To effectively implement Axios interceptors, the recommended approach is to create and configure a dedicated Axios instance. This ensures that all requests made through this instance automatically benefit from your centralized interceptor logic.

1. Create an Axios Instance

Begin by creating a new file (e.g., axiosInstance.js or apiClient.js) to house your configured Axios instance. This file will serve as the single source for your application's HTTP client configuration.

// axiosInstance.js
import axios from 'axios';

const instance = axios.create({
  baseURL: 'https://api.example.com', // Replace with your API's base URL
  timeout: 10000, // Request timeout in milliseconds (e.g., 10 seconds)
  headers: {
    'Content-Type': 'application/json',
    // Default headers can be set here. Authorization typically added via interceptor.
  }
});

export default instance;
  • baseURL: Specifies the base URL for all requests made with this instance, simplifying path definitions.
  • timeout: Sets a maximum time for the request to complete, preventing indefinite waits.
  • headers: Allows you to define default headers that will be sent with every request.

2. Add Request Interceptors

Request interceptors are ideal for modifying outgoing requests, such as attaching authentication tokens, adding specific headers, or logging request details.

// Inside axiosInstance.js, after creating the 'instance'
instance.interceptors.request.use(
  (config) => {
    // Customize the request config here.
    // Example: Add an Authorization token from local storage or a global state.
    const token = localStorage.getItem('authToken');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }

    console.log(`Request: ${config.method?.toUpperCase()} ${config.url}`);
    // You could also show a global loading indicator here.
    // store.dispatch(startLoading());

    return config; // Always return the config object (modified or original)
  },
  (error) => {
    // Handle request errors (e.g., network issues before sending)
    console.error('Request Error:', error);
    // store.dispatch(stopLoading());
    return Promise.reject(error); // Propagate the error
  }
);
  • The first function in use receives the config object and should return it.
  • The second function handles any errors that occur during the request setup.

3. Add Response Interceptors

Response interceptors process responses after they're received but before they reach your application logic. This is perfect for global error handling, refreshing expired tokens, or data transformation.

// Inside axiosInstance.js, after adding request interceptors
instance.interceptors.response.use(
  (response) => {
    // Any status code within the 2xx range triggers this function.
    // Perform actions on successful response data.
    console.log(`Response: ${response.status} ${response.config.url}`);
    // store.dispatch(stopLoading());
    return response; // Always return the response object
  },
  async (error) => {
    // Any status code outside the 2xx range triggers this function.
    console.error('Response Error:', error.response ? error.response.status : error.message);
    // store.dispatch(stopLoading());

    const originalRequest = error.config;

    // Example: Handle 401 Unauthorized errors and token refresh
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true; // Mark to prevent infinite loops
      try {
        // Assume you have a function to get a new access token
        const newAccessToken = await fetchNewAccessToken(); // Implement your token refresh logic
        localStorage.setItem('authToken', newAccessToken); // Store new token
        originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
        return instance(originalRequest); // Retry the original request with the new token
      } catch (refreshError) {
        console.error('Token refresh failed:', refreshError);
        // Redirect to login or clear authentication state if refresh fails
        // window.location.href = '/login';
        return Promise.reject(refreshError);
      }
    }

    // Handle other errors (e.g., 403, 404, 500)
    // You can display toast messages or log errors to a central service here.
    return Promise.reject(error); // Propagate the error
  }
);
  • The first function handles successful responses (status codes 2xx).
  • The second function handles response errors (non-2xx status codes or network issues).

4. Use the Axios Instance

After setting up axiosInstance.js, you can import and use this instance across your application, whether in Next.js pages or components, or any other JavaScript environment.

// In your application code (e.g., a service file, a component, or a Next.js page)
import apiClient from './axiosInstance'; // Adjust the import path as necessary

async function getUserProfile(userId) {
  try {
    const response = await apiClient.get(`/users/${userId}`);
    return response.data;
  } catch (error) {
    console.error('Failed to fetch user profile:', error);
    throw error; // Re-throw to allow further handling in the calling context
  }
}

// Example usage:
// (async () => {
//   try {
//     const user = await getUserProfile('123');
//     console.log('User data:', user);
//   } catch (err) {
//     console.error('An error occurred:', err);
//   }
// })();

By using apiClient instead of the global axios object, all requests made will automatically pass through the interceptors you've configured.

5. Customizing the Interceptor

Interceptors offer extensive customization possibilities. Beyond basic token handling and logging, you can implement sophisticated logic such as:

  • Data Serialization/Deserialization: Automatically convert data formats (e.g., camelCase to snake_case for backend).
  • Global Error Notifications: Display user-friendly messages (e.g., toast notifications) for specific API error codes.
  • Request Tracing: Add unique X-Request-ID headers to requests for easier debugging and monitoring across microservices.
  • Conditional Logic: Apply interceptor actions only if certain conditions are met (e.g., only for specific URLs or HTTP methods).
// Example of customization: Adding a unique request ID
instance.interceptors.request.use((config) => {
  if (!config.headers['X-Request-ID']) { // Add only if not already present
    config.headers['X-Request-ID'] = `req-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
  }
  return config;
});

Common Use Cases for Axios Interceptors

Interceptors streamline many common web application tasks:

Interceptor Type Use Case Example Action
Request Authentication Automatically add JWT or Bearer tokens to Authorization headers.
Request Logging & Debugging Log details of outgoing requests (method, URL, headers).
Request Data Transformation Standardize request payload format (e.g., date formats).
Request Loading Indicators Show a global loading spinner when an API call starts.
Response Global Error Handling Catch HTTP status codes (401, 403, 404, 500) and display user-friendly messages.
Response Token Refresh Automatically refresh expired access tokens using a refresh token.
Response Data Transformation Unwrap nested data (response.data.data) or convert response formats.
Response Loading Indicators Hide a global loading spinner when an API call completes or fails.
Response Session Management Check for session expiration headers or update local session state.

Error Handling with Interceptors

Centralizing error handling with response interceptors significantly reduces boilerplate code and improves consistency.

// Enhanced response error handler (from Step 3)
async (error) => {
  if (error.response) {
    // The server responded with a status code outside of 2xx
    switch (error.response.status) {
      case 400:
        console.error('Bad Request:', error.response.data.message || 'Invalid data sent.');
        // displayToast('Validation Error: ' + (error.response.data.message || 'Please check your input.'));
        break;
      case 401:
        console.error('Unauthorized: Session expired or invalid token.');
        // Consider triggering a token refresh or redirecting to login
        break;
      case 403:
        console.error('Forbidden: You do not have permission to access this resource.');
        // displayToast('Permission Denied.');
        break;
      case 404:
        console.error('Not Found: The requested resource does not exist.');
        // displayToast('Resource not found.');
        break;
      case 500:
        console.error('Server Error: Something went wrong on the server.');
        // displayToast('Server Error: Please try again later.');
        break;
      default:
        console.error(`Unhandled API Error: ${error.response.status}`, error.response.data);
    }
  } else if (error.request) {
    // The request was made but no response was received (e.g., network down)
    console.error('Network Error: No response received from server.');
    // displayToast('Network Error: Please check your internet connection.');
  } else {
    // Something happened in setting up the request that triggered an Error
    console.error('Request Setup Error:', error.message);
  }
  return Promise.reject(error); // Crucial for propagating the error
}

Removing Interceptors

While typically persistent, you can remove an interceptor if needed. The use method returns an id that can be used with the eject method.

// Add an interceptor and store its ID
const requestInterceptorId = instance.interceptors.request.use((config) => {
  // ... interceptor logic ...
  return config;
});

// Later, remove it using the stored ID
instance.interceptors.request.eject(requestInterceptorId);

Best Practices

  • Centralize Your API Client: Always create a single axiosInstance.js file for all API configurations and interceptors.
  • Single Responsibility: Design each interceptor to handle a specific concern (e.g., one for authentication, another for logging, one for error display).
  • Propagate Errors: Always return Promise.reject(error) in interceptor error handlers to ensure the error continues down the promise chain.
  • Prevent Infinite Loops: For token refresh logic, use a flag (e.g., originalRequest._retry) to prevent repeated retries if the refresh token process itself fails.
  • Use Conditional Logic: Implement if statements within interceptors to apply logic only when specific conditions are met, such as for particular endpoints or request types.

By following these guidelines, you can effectively leverage Axios interceptors to build robust, maintainable, and efficient network communication layers in your applications.