1. api-client.ts : axios interceptor
import axios, { type AxiosRequestConfig, type AxiosResponse, AxiosError } from 'axios';
import HttpStatus from 'http-status-codes';
import { toast } from 'react-toastify';

const BASE_URL =   'your-base-url';

// ===========> Axios instance <============
const apiClient = axios.create({
  baseURL: BASE_URL,
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  },
});

// ====== Throttle Network Error Toast (once every 60 seconds) ======
const TOAST_THROTTLE_TIME = 60000;

const hasNetworkErrorToastShownRecently = () => {
  const lastShown = sessionStorage.getItem('lastNetworkErrorToast');
  return lastShown && Date.now() - Number(lastShown) < TOAST_THROTTLE_TIME;
};

const setNetworkErrorToastTimestamp = () => {
  sessionStorage.setItem('lastNetworkErrorToast', Date.now().toString());
};

let loginToastShown = false;

// ==========> Interceptor <================
apiClient.interceptors.response.use(
  (response: AxiosResponse) => response.data,
  (error: AxiosError) => {
    const status = error.response?.status || HttpStatus.INTERNAL_SERVER_ERROR;
    const message = HttpStatus.getStatusText(status) || 'An unknown error occurred';

    if (status === HttpStatus.FORBIDDEN) {
       // some logout logic or maybe you want to do something different.
      }
      return Promise.reject({ status, message, data: null });
    }

    // Handle network errors (only show once every 60 seconds)
    if (!error.response || error.code === 'ECONNABORTED') {
      if (!hasNetworkErrorToastShownRecently()) {
        toast.error('Network error! Please check your internet connection.');
        setNetworkErrorToastTimestamp();
      }
      return Promise.reject({ status, message, data: null });
    }

    const data: any = error.response?.data;
    if (data && !data.success && Array.isArray(data.errors)) {
      console.error('Validation Errors:', data.errors);
      data.errors.forEach((err: any) => {
        toast.error(err.msg || 'Validation error');
      });
      return;
    }
    
    return Promise.reject({
      status,
      message,
      data: error.response?.data || null,
    });
  }
);

export const apiRequest = async (
  method: 'GET' | 'POST' | 'PUT' | 'DELETE',
  url: string,
  data?: any,
  config: AxiosRequestConfig = {}
) => {
  try {
    const response = await apiClient({
      method,
      url,
      data,
      ...config,
    });
    return response;
  } catch (error) {
    throw error;
  }
};

export default apiClient;
  1. useAxios.ts : a hook which exposes data, loading and error and lot of options.
import { useState, useEffect, useCallback } from "react";
import { AxiosError } from "axios";
import { apiRequest } from "../lib";
import { type UseAxiosProps, type UseAxiosReturn } from "../types";
import { GlobalVariable } from "../global";

export const useAxios = <T>({
    method = "GET",
    url,
    body,
    config = {},
    headers = {},
    autoFetch = false,
    shouldFetch = true,
}: UseAxiosProps): UseAxiosReturn<T> => {

    const [data, setData] = useState<T | null>(null);
    const [loading, setLoading] = useState<boolean>(false);
    const [error, setError] = useState<AxiosError | null>(null);

    const fetchData = useCallback(
        async (overrideBody: object | null = null, overrideHeaders: Record<string, string> = {}, overrideUrl: string | null = null) => {
            if (!shouldFetch) return null;
            if (!(["/login"].includes(url?.trim() || "")) && !(localStorage.getItem(GlobalVariable.EXECUTIVE_TOKEN_PATH))) return;
            setLoading(true);
            setError(null);
            try {
                const response = await apiRequest(method, overrideUrl || url || "", overrideBody || body, {
                    ...config,
                    headers: {
                        ...((url !== '/login') && { Authorization: `Bearer ${localStorage.getItem(GlobalVariable.EXECUTIVE_TOKEN_PATH)}`}),
                        ...headers,
                        ...overrideHeaders,
                    },
                });
                setData(response as T);
                return response as T;
            } catch (err) {
                setError(err as AxiosError);
                // @ts-ignore
                return err.data;
            } finally {
                setLoading(false);
            }
        },
        [method, url, body, config, headers, shouldFetch]
    );

    useEffect(() => {
        if (autoFetch && shouldFetch) {
            fetchData();
        }
    }, [autoFetch, shouldFetch, url, method]);

    return { data, loading, error, fetchData };
};
  1. types.ts : basic types
import { AxiosError, type AxiosRequestConfig } from "axios";
export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";

export interface UseAxiosProps {
    method?: HttpMethod;
    url?: string;
    body?: object;
    config?: AxiosRequestConfig;
    headers?: Record<string, string>;
    autoFetch?: boolean;
    shouldFetch?: boolean;
    scope ?: string;
}

export interface UseAxiosReturn<T> {
    data: T | null;
    loading: boolean;
    error: AxiosError | null;
    fetchData: (overrideBody?: object, overrideHeaders?: Record<string, string>, overrideUrl?: string) => Promise<T | null>;
}