Skip to main content

Instance management

Create dedicated instances

Create separate axios instances for different APIs or services instead of using the global axios instance.
// api/github.js
import axios from 'axios';

export const githubApi = axios.create({
  baseURL: 'https://api.github.com',
  timeout: 10000,
  headers: {
    Accept: 'application/vnd.github.v3+json'
  }
});

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

export const backendApi = axios.create({
  baseURL: process.env.API_URL,
  timeout: 5000,
  headers: {
    'Content-Type': 'application/json'
  }
});
Using dedicated instances makes it easier to configure different timeouts, base URLs, and interceptors for each service.

Organize by feature

// services/user.service.js
import { backendApi } from '../api/backend';

export const userService = {
  getUser: (id) => backendApi.get(`/users/${id}`),
  updateUser: (id, data) => backendApi.put(`/users/${id}`, data),
  deleteUser: (id) => backendApi.delete(`/users/${id}`)
};

// services/auth.service.js
import { backendApi } from '../api/backend';

export const authService = {
  login: (credentials) => backendApi.post('/auth/login', credentials),
  logout: () => backendApi.post('/auth/logout'),
  refreshToken: () => backendApi.post('/auth/refresh')
};

Error handling

Centralized error handling

Implement global error handling with interceptors:
import axios from 'axios';

const api = axios.create({
  baseURL: process.env.API_URL
});

// Response interceptor for error handling
api.interceptors.response.use(
  response => response,
  error => {
    // Network errors
    if (!error.response) {
      console.error('Network error:', error.message);
      // Show notification: "Network error. Please check your connection."
      return Promise.reject(error);
    }

    // HTTP errors
    switch (error.response.status) {
      case 401:
        // Redirect to login
        window.location.href = '/login';
        break;
      case 403:
        // Show forbidden message
        console.error('Access denied');
        break;
      case 404:
        // Handle not found
        console.error('Resource not found');
        break;
      case 500:
        // Show server error message
        console.error('Server error');
        break;
      default:
        console.error('Request failed:', error.message);
    }

    return Promise.reject(error);
  }
);

Always handle errors locally

Even with global error handling, always handle errors at the call site:
async function fetchUserData(userId) {
  try {
    const response = await api.get(`/users/${userId}`);
    return { data: response.data, error: null };
  } catch (error) {
    // Provide context-specific error handling
    console.error(`Failed to fetch user ${userId}:`, error);
    return { data: null, error: error.message };
  }
}

Use isAxiosError for type checking

import axios from 'axios';

try {
  await api.get('/data');
} catch (error) {
  if (axios.isAxiosError(error)) {
    // TypeScript knows this is an AxiosError
    console.error('Status:', error.response?.status);
    console.error('Data:', error.response?.data);
  } else {
    // Handle non-Axios errors
    console.error('Unexpected error:', error);
  }
}

Authentication

Token management with interceptors

const api = axios.create({
  baseURL: process.env.API_URL
});

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

// Response interceptor for token refresh
let isRefreshing = false;
let failedQueue = [];

const processQueue = (error, token = null) => {
  failedQueue.forEach(prom => {
    if (error) {
      prom.reject(error);
    } else {
      prom.resolve(token);
    }
  });
  failedQueue = [];
};

api.interceptors.response.use(
  response => response,
  async error => {
    const originalRequest = error.config;

    if (error.response?.status === 401 && !originalRequest._retry) {
      if (isRefreshing) {
        return new Promise((resolve, reject) => {
          failedQueue.push({ resolve, reject });
        })
          .then(token => {
            originalRequest.headers.Authorization = `Bearer ${token}`;
            return api(originalRequest);
          })
          .catch(err => Promise.reject(err));
      }

      originalRequest._retry = true;
      isRefreshing = true;

      try {
        const refreshToken = localStorage.getItem('refreshToken');
        const response = await axios.post('/auth/refresh', { refreshToken });
        const { token } = response.data;
        
        localStorage.setItem('token', token);
        api.defaults.headers.common.Authorization = `Bearer ${token}`;
        
        processQueue(null, token);
        return api(originalRequest);
      } catch (refreshError) {
        processQueue(refreshError, null);
        localStorage.removeItem('token');
        localStorage.removeItem('refreshToken');
        window.location.href = '/login';
        return Promise.reject(refreshError);
      } finally {
        isRefreshing = false;
      }
    }

    return Promise.reject(error);
  }
);

Request optimization

Use request cancellation

Cancel requests when they’re no longer needed:
import { useEffect, useState } from 'react';
import axios from 'axios';

function SearchComponent() {
  const [results, setResults] = useState([]);
  const [query, setQuery] = useState('');

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

    const fetchResults = async () => {
      if (!query) return;
      
      try {
        const response = await axios.get('/api/search', {
          params: { q: query },
          signal: controller.signal
        });
        setResults(response.data);
      } catch (error) {
        if (axios.isCancel(error)) {
          console.log('Request cancelled');
        } else {
          console.error('Search failed:', error);
        }
      }
    };

    fetchResults();

    // Cleanup: cancel request when query changes
    return () => controller.abort();
  }, [query]);

  return (
    <input
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      placeholder="Search..."
    />
  );
}

Implement request deduplication

Prevent duplicate concurrent requests:
const pendingRequests = new Map();

function createRequestKey(config) {
  return `${config.method}-${config.url}-${JSON.stringify(config.params)}`;
}

api.interceptors.request.use(config => {
  const key = createRequestKey(config);
  
  if (pendingRequests.has(key)) {
    // Return existing promise for this request
    const controller = new AbortController();
    config.signal = controller.signal;
    controller.abort(); // Cancel duplicate
    return config;
  }
  
  pendingRequests.set(key, true);
  return config;
});

api.interceptors.response.use(
  response => {
    const key = createRequestKey(response.config);
    pendingRequests.delete(key);
    return response;
  },
  error => {
    if (error.config) {
      const key = createRequestKey(error.config);
      pendingRequests.delete(key);
    }
    return Promise.reject(error);
  }
);

Use timeouts appropriately

// Different timeouts for different operations
const api = axios.create({
  baseURL: process.env.API_URL,
  timeout: 5000 // Default 5 seconds
});

// Quick operations
api.get('/health', { timeout: 2000 });

// Long operations
api.post('/reports/generate', data, { timeout: 30000 });

// No timeout for uploads
api.post('/files/upload', formData, { timeout: 0 });

TypeScript integration

Define response types

interface User {
  id: number;
  name: string;
  email: string;
}

interface ApiResponse<T> {
  data: T;
  message: string;
  status: number;
}

// Typed API calls
const getUser = async (id: number): Promise<User> => {
  const response = await api.get<ApiResponse<User>>(`/users/${id}`);
  return response.data.data;
};

const getUsers = async (): Promise<User[]> => {
  const response = await api.get<ApiResponse<User[]>>('/users');
  return response.data.data;
};

Create typed API client

class ApiClient {
  private client: AxiosInstance;

  constructor(baseURL: string) {
    this.client = axios.create({ baseURL });
    this.setupInterceptors();
  }

  private setupInterceptors(): void {
    this.client.interceptors.request.use(
      config => {
        const token = localStorage.getItem('token');
        if (token) {
          config.headers.Authorization = `Bearer ${token}`;
        }
        return config;
      }
    );
  }

  async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
    const response = await this.client.get<T>(url, config);
    return response.data;
  }

  async post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    const response = await this.client.post<T>(url, data, config);
    return response.data;
  }
}

const api = new ApiClient(process.env.API_URL!);

Testing

Mock axios in tests

import axios from 'axios';
import { userService } from './user.service';

jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;

describe('userService', () => {
  it('should fetch user data', async () => {
    const mockUser = { id: 1, name: 'John' };
    mockedAxios.get.mockResolvedValue({ data: mockUser });

    const user = await userService.getUser(1);
    
    expect(user).toEqual(mockUser);
    expect(mockedAxios.get).toHaveBeenCalledWith('/users/1');
  });

  it('should handle errors', async () => {
    mockedAxios.get.mockRejectedValue(new Error('Network error'));

    await expect(userService.getUser(1)).rejects.toThrow('Network error');
  });
});

Configuration management

Environment-based configuration

// config/api.config.js
const configs = {
  development: {
    baseURL: 'http://localhost:3000/api',
    timeout: 10000,
    headers: { 'X-Environment': 'dev' }
  },
  staging: {
    baseURL: 'https://staging-api.example.com',
    timeout: 8000,
    headers: { 'X-Environment': 'staging' }
  },
  production: {
    baseURL: 'https://api.example.com',
    timeout: 5000,
    headers: { 'X-Environment': 'prod' }
  }
};

const env = process.env.NODE_ENV || 'development';
export const apiConfig = configs[env];

// Usage
import axios from 'axios';
import { apiConfig } from './config/api.config';

const api = axios.create(apiConfig);

Error handling

Comprehensive error handling guide

Interceptors

Learn about request and response interceptors

TypeScript

TypeScript integration guide

Cancellation

Request cancellation patterns

Build docs developers (and LLMs) love