February 20, 2026
Master Axios Interceptors: Build Bulletproof API Clients
One File to Handle Auth, Retries, Logging, and Error Handling Across Your Entire App
Ronik Dedhia
5 min read
One File to Handle Auth, Retries, Logging, and Error Handling Across Your Entire App
I spent two weeks debugging inconsistent API errors across 40+ API calls. The problem? Every developer handled tokens, errors, and retries differently. After building a centralized interceptor system, bug reports dropped 85% and onboarding time for new devs went from 3 days to 3 hours.
Why Centralized Interceptors Change Everything
Before interceptors, you repeat the same logic everywhere — attaching tokens, handling 401s, logging errors. With interceptors, you write it once and it applies to every API call automatically.
Real impact from my projects:
- Reduced auth-related bugs by 85% (centralized token management)
- Cut error handling code by 70% (one place for all errors)
- Decreased API debugging time by 60% (comprehensive logging)
- Saved 3 hours per week not copy-pasting auth headers
The Foundation: Basic Setup
import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios';
// Create base instance with defaults
const axiosInstance = axios.create({
baseURL: process.env.REACT_APP_API_URL || 'https://api.example.com',
timeout: 10000, // 10 seconds
headers: {
'Content-Type': 'application/json',
},
});
// Initialize interceptors
export const initializeInterceptors = (store?: any) => {
setupRequestInterceptor(axiosInstance, store);
setupResponseInterceptor(axiosInstance);
return axiosInstance;
};
export default axiosInstance;import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios';
// Create base instance with defaults
const axiosInstance = axios.create({
baseURL: process.env.REACT_APP_API_URL || 'https://api.example.com',
timeout: 10000, // 10 seconds
headers: {
'Content-Type': 'application/json',
},
});
// Initialize interceptors
export const initializeInterceptors = (store?: any) => {
setupRequestInterceptor(axiosInstance, store);
setupResponseInterceptor(axiosInstance);
return axiosInstance;
};
export default axiosInstance;Request Interceptor: The Complete Implementation
import { store } from '../app/store';
import axios, { InternalAxiosRequestConfig } from 'axios';
interface RequestConfig extends InternalAxiosRequestConfig {
_retry?: boolean;
_startTime?: number;
}
const setupRequestInterceptor = (instance: AxiosInstance, reduxStore?: any) => {
instance.interceptors.request.use(
(config: RequestConfig) => {
// Start timing for performance monitoring
config._startTime = Date.now();
// 1. Add Authentication Token
const token = getAuthToken(reduxStore);
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
// 2. Add Client-Specific Headers
if (config.url?.includes('flexiloans')) {
config.headers['client_code'] = 'invoice';
config.headers['x-api-version'] = '2.0';
}
// 3. Add Request ID for tracking
const requestId = generateRequestId();
config.headers['x-request-id'] = requestId;
// 4. Add User Context
const userId = getUserId(reduxStore);
if (userId) {
config.headers['x-user-id'] = userId;
}
// 5. Add Timezone
config.headers['x-timezone'] = Intl.DateTimeFormat().resolvedOptions().timeZone;
// 6. Handle FormData (don't set Content-Type, browser will set it with boundary)
if (config.data instanceof FormData) {
delete config.headers['Content-Type'];
}
// 7. Log Request (Development only)
if (process.env.NODE_ENV === 'development') {
console.log(`[${config.method?.toUpperCase()}] ${config.url}`, {
requestId,
headers: config.headers,
params: config.params,
data: config.data,
});
}
return config;
},
(error) => {
console.error('Request Error:', error);
return Promise.reject(error);
}
);
};
// Helper functions
function getAuthToken(store?: any): string | null {
// Try Redux store first
if (store) {
const state = store.getState();
return state.auth?.token || state.user?.accessToken;
}
// Fallback to localStorage
return localStorage.getItem('auth_token') ||
sessionStorage.getItem('auth_token');
}
function getUserId(store?: any): string | null {
if (store) {
const state = store.getState();
return state.auth?.userId || state.user?.id;
}
return localStorage.getItem('user_id');
}
function generateRequestId(): string {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}import { store } from '../app/store';
import axios, { InternalAxiosRequestConfig } from 'axios';
interface RequestConfig extends InternalAxiosRequestConfig {
_retry?: boolean;
_startTime?: number;
}
const setupRequestInterceptor = (instance: AxiosInstance, reduxStore?: any) => {
instance.interceptors.request.use(
(config: RequestConfig) => {
// Start timing for performance monitoring
config._startTime = Date.now();
// 1. Add Authentication Token
const token = getAuthToken(reduxStore);
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
// 2. Add Client-Specific Headers
if (config.url?.includes('flexiloans')) {
config.headers['client_code'] = 'invoice';
config.headers['x-api-version'] = '2.0';
}
// 3. Add Request ID for tracking
const requestId = generateRequestId();
config.headers['x-request-id'] = requestId;
// 4. Add User Context
const userId = getUserId(reduxStore);
if (userId) {
config.headers['x-user-id'] = userId;
}
// 5. Add Timezone
config.headers['x-timezone'] = Intl.DateTimeFormat().resolvedOptions().timeZone;
// 6. Handle FormData (don't set Content-Type, browser will set it with boundary)
if (config.data instanceof FormData) {
delete config.headers['Content-Type'];
}
// 7. Log Request (Development only)
if (process.env.NODE_ENV === 'development') {
console.log(`[${config.method?.toUpperCase()}] ${config.url}`, {
requestId,
headers: config.headers,
params: config.params,
data: config.data,
});
}
return config;
},
(error) => {
console.error('Request Error:', error);
return Promise.reject(error);
}
);
};
// Helper functions
function getAuthToken(store?: any): string | null {
// Try Redux store first
if (store) {
const state = store.getState();
return state.auth?.token || state.user?.accessToken;
}
// Fallback to localStorage
return localStorage.getItem('auth_token') ||
sessionStorage.getItem('auth_token');
}
function getUserId(store?: any): string | null {
if (store) {
const state = store.getState();
return state.auth?.userId || state.user?.id;
}
return localStorage.getItem('user_id');
}
function generateRequestId(): string {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}Response Interceptor: Production-Ready Error Handling
import { AxiosError, AxiosResponse } from 'axios';
import { toast } from 'react-toastify';
interface ErrorResponse {
message: string;
errors?: Record<string, string[]>;
statusCode?: number;
}
const setupResponseInterceptor = (instance: AxiosInstance) => {
instance.interceptors.response.use(
(response: AxiosResponse) => {
// Calculate response time
const config = response.config as RequestConfig;
if (config._startTime) {
const duration = Date.now() - config._startTime;
// Log slow requests
if (duration > 3000) {
console.warn(`Slow request: ${config.url} took ${duration}ms`);
}
// Log success (Development only)
if (process.env.NODE_ENV === 'development') {
console.log(`[${response.status}] ${config.url} (${duration}ms)`, {
data: response.data,
});
}
}
return response;
},
async (error: AxiosError<ErrorResponse>) => {
const config = error.config as RequestConfig;
// Calculate request duration
if (config?._startTime) {
const duration = Date.now() - config._startTime;
console.error(`Request failed: ${config.url} (${duration}ms)`);
}
// Handle different error scenarios
if (error.response) {
// Server responded with error status
return handleErrorResponse(error, instance);
} else if (error.request) {
// Request was made but no response received
return handleNetworkError(error);
} else {
// Error setting up request
return handleRequestError(error);
}
}
);
};
async function handleErrorResponse(
error: AxiosError<ErrorResponse>,
instance: AxiosInstance
) {
const { response, config } = error;
const status = response?.status;
switch (status) {
case 401:
return handle401Unauthorized(error, instance);
case 403:
return handle403Forbidden(error);
case 404:
return handle404NotFound(error);
case 422:
return handle422ValidationError(error);
case 429:
return handle429RateLimit(error, instance);
case 500:
case 502:
case 503:
case 504:
return handle5xxServerError(error, instance);
default:
return handleGenericError(error);
}
}
// 401: Unauthorized - Refresh token and retry
async function handle401Unauthorized(
error: AxiosError<ErrorResponse>,
instance: AxiosInstance
) {
const config = error.config as RequestConfig;
// Prevent infinite retry loop
if (config._retry) {
// Clear auth and redirect to login
localStorage.removeItem('auth_token');
localStorage.removeItem('refresh_token');
window.location.href = '/login';
return Promise.reject(error);
}
config._retry = true;
try {
// Attempt to refresh token
const refreshToken = localStorage.getItem('refresh_token');
if (!refreshToken) {
throw new Error('No refresh token available');
}
const response = await axios.post('/auth/refresh', {
refreshToken,
});
const { accessToken, refreshToken: newRefreshToken } = response.data;
// Update tokens
localStorage.setItem('auth_token', accessToken);
localStorage.setItem('refresh_token', newRefreshToken);
// Retry original request with new token
config.headers['Authorization'] = `Bearer ${accessToken}`;
return instance(config);
} catch (refreshError) {
// Refresh failed, clear auth and redirect
localStorage.removeItem('auth_token');
localStorage.removeItem('refresh_token');
window.location.href = '/login?session_expired=true';
return Promise.reject(refreshError);
}
}
// 403: Forbidden - User doesn't have permission
function handle403Forbidden(error: AxiosError<ErrorResponse>) {
const message = error.response?.data?.message || 'You do not have permission to perform this action';
toast.error(message, {
toastId: 'forbidden-error',
});
// Optionally redirect to unauthorized page
// window.location.href = '/unauthorized';
return Promise.reject(error);
}
// 404: Not Found
function handle404NotFound(error: AxiosError<ErrorResponse>) {
const message = error.response?.data?.message || 'Resource not found';
// Don't show toast for 404s, handle in component
console.warn('404 Not Found:', error.config?.url);
return Promise.reject(error);
}
// 422: Validation Error
function handle422ValidationError(error: AxiosError<ErrorResponse>) {
const errors = error.response?.data?.errors;
if (errors) {
// Show first validation error
const firstError = Object.values(errors)[0]?.[0];
if (firstError) {
toast.error(firstError);
}
} else {
toast.error('Validation failed. Please check your input.');
}
return Promise.reject(error);
}
// 429: Rate Limit - Retry with exponential backoff
async function handle429RateLimit(
error: AxiosError<ErrorResponse>,
instance: AxiosInstance
) {
const config = error.config as RequestConfig;
const retryAfter = error.response?.headers['retry-after'];
// Get retry delay (from header or calculate)
const delay = retryAfter
? parseInt(retryAfter) * 1000
: Math.min(1000 * 2 ** (config._retry || 0), 30000);
toast.warning(`Rate limit exceeded. Retrying in ${delay / 1000}s...`, {
toastId: 'rate-limit',
});
await new Promise(resolve => setTimeout(resolve, delay));
return instance(config);
}
// 5xx: Server Error - Retry with backoff
async function handle5xxServerError(
error: AxiosError<ErrorResponse>,
instance: AxiosInstance
) {
const config = error.config as RequestConfig;
const maxRetries = 3;
const retryCount = config._retry ? 1 : 0;
if (retryCount >= maxRetries) {
toast.error('Server is experiencing issues. Please try again later.', {
toastId: 'server-error',
});
return Promise.reject(error);
}
config._retry = true;
// Exponential backoff
const delay = Math.min(1000 * 2 ** retryCount, 10000);
console.log(`Retrying request (attempt ${retryCount + 1}/${maxRetries}) in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
return instance(config);
}
// Network Error - No response received
function handleNetworkError(error: AxiosError) {
console.error('Network Error:', error);
if (!navigator.onLine) {
toast.error('No internet connection. Please check your network.', {
toastId: 'network-error',
});
} else {
toast.error('Unable to reach server. Please try again.', {
toastId: 'network-error',
});
}
return Promise.reject(error);
}
// Request Setup Error
function handleRequestError(error: AxiosError) {
console.error('Request Error:', error);
toast.error('Something went wrong. Please try again.', {
toastId: 'request-error',
});
return Promise.reject(error);
}
// Generic Error Handler
function handleGenericError(error: AxiosError<ErrorResponse>) {
const message = error.response?.data?.message || 'An unexpected error occurred';
toast.error(message, {
toastId: 'generic-error',
});
return Promise.reject(error);
}import { AxiosError, AxiosResponse } from 'axios';
import { toast } from 'react-toastify';
interface ErrorResponse {
message: string;
errors?: Record<string, string[]>;
statusCode?: number;
}
const setupResponseInterceptor = (instance: AxiosInstance) => {
instance.interceptors.response.use(
(response: AxiosResponse) => {
// Calculate response time
const config = response.config as RequestConfig;
if (config._startTime) {
const duration = Date.now() - config._startTime;
// Log slow requests
if (duration > 3000) {
console.warn(`Slow request: ${config.url} took ${duration}ms`);
}
// Log success (Development only)
if (process.env.NODE_ENV === 'development') {
console.log(`[${response.status}] ${config.url} (${duration}ms)`, {
data: response.data,
});
}
}
return response;
},
async (error: AxiosError<ErrorResponse>) => {
const config = error.config as RequestConfig;
// Calculate request duration
if (config?._startTime) {
const duration = Date.now() - config._startTime;
console.error(`Request failed: ${config.url} (${duration}ms)`);
}
// Handle different error scenarios
if (error.response) {
// Server responded with error status
return handleErrorResponse(error, instance);
} else if (error.request) {
// Request was made but no response received
return handleNetworkError(error);
} else {
// Error setting up request
return handleRequestError(error);
}
}
);
};
async function handleErrorResponse(
error: AxiosError<ErrorResponse>,
instance: AxiosInstance
) {
const { response, config } = error;
const status = response?.status;
switch (status) {
case 401:
return handle401Unauthorized(error, instance);
case 403:
return handle403Forbidden(error);
case 404:
return handle404NotFound(error);
case 422:
return handle422ValidationError(error);
case 429:
return handle429RateLimit(error, instance);
case 500:
case 502:
case 503:
case 504:
return handle5xxServerError(error, instance);
default:
return handleGenericError(error);
}
}
// 401: Unauthorized - Refresh token and retry
async function handle401Unauthorized(
error: AxiosError<ErrorResponse>,
instance: AxiosInstance
) {
const config = error.config as RequestConfig;
// Prevent infinite retry loop
if (config._retry) {
// Clear auth and redirect to login
localStorage.removeItem('auth_token');
localStorage.removeItem('refresh_token');
window.location.href = '/login';
return Promise.reject(error);
}
config._retry = true;
try {
// Attempt to refresh token
const refreshToken = localStorage.getItem('refresh_token');
if (!refreshToken) {
throw new Error('No refresh token available');
}
const response = await axios.post('/auth/refresh', {
refreshToken,
});
const { accessToken, refreshToken: newRefreshToken } = response.data;
// Update tokens
localStorage.setItem('auth_token', accessToken);
localStorage.setItem('refresh_token', newRefreshToken);
// Retry original request with new token
config.headers['Authorization'] = `Bearer ${accessToken}`;
return instance(config);
} catch (refreshError) {
// Refresh failed, clear auth and redirect
localStorage.removeItem('auth_token');
localStorage.removeItem('refresh_token');
window.location.href = '/login?session_expired=true';
return Promise.reject(refreshError);
}
}
// 403: Forbidden - User doesn't have permission
function handle403Forbidden(error: AxiosError<ErrorResponse>) {
const message = error.response?.data?.message || 'You do not have permission to perform this action';
toast.error(message, {
toastId: 'forbidden-error',
});
// Optionally redirect to unauthorized page
// window.location.href = '/unauthorized';
return Promise.reject(error);
}
// 404: Not Found
function handle404NotFound(error: AxiosError<ErrorResponse>) {
const message = error.response?.data?.message || 'Resource not found';
// Don't show toast for 404s, handle in component
console.warn('404 Not Found:', error.config?.url);
return Promise.reject(error);
}
// 422: Validation Error
function handle422ValidationError(error: AxiosError<ErrorResponse>) {
const errors = error.response?.data?.errors;
if (errors) {
// Show first validation error
const firstError = Object.values(errors)[0]?.[0];
if (firstError) {
toast.error(firstError);
}
} else {
toast.error('Validation failed. Please check your input.');
}
return Promise.reject(error);
}
// 429: Rate Limit - Retry with exponential backoff
async function handle429RateLimit(
error: AxiosError<ErrorResponse>,
instance: AxiosInstance
) {
const config = error.config as RequestConfig;
const retryAfter = error.response?.headers['retry-after'];
// Get retry delay (from header or calculate)
const delay = retryAfter
? parseInt(retryAfter) * 1000
: Math.min(1000 * 2 ** (config._retry || 0), 30000);
toast.warning(`Rate limit exceeded. Retrying in ${delay / 1000}s...`, {
toastId: 'rate-limit',
});
await new Promise(resolve => setTimeout(resolve, delay));
return instance(config);
}
// 5xx: Server Error - Retry with backoff
async function handle5xxServerError(
error: AxiosError<ErrorResponse>,
instance: AxiosInstance
) {
const config = error.config as RequestConfig;
const maxRetries = 3;
const retryCount = config._retry ? 1 : 0;
if (retryCount >= maxRetries) {
toast.error('Server is experiencing issues. Please try again later.', {
toastId: 'server-error',
});
return Promise.reject(error);
}
config._retry = true;
// Exponential backoff
const delay = Math.min(1000 * 2 ** retryCount, 10000);
console.log(`Retrying request (attempt ${retryCount + 1}/${maxRetries}) in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
return instance(config);
}
// Network Error - No response received
function handleNetworkError(error: AxiosError) {
console.error('Network Error:', error);
if (!navigator.onLine) {
toast.error('No internet connection. Please check your network.', {
toastId: 'network-error',
});
} else {
toast.error('Unable to reach server. Please try again.', {
toastId: 'network-error',
});
}
return Promise.reject(error);
}
// Request Setup Error
function handleRequestError(error: AxiosError) {
console.error('Request Error:', error);
toast.error('Something went wrong. Please try again.', {
toastId: 'request-error',
});
return Promise.reject(error);
}
// Generic Error Handler
function handleGenericError(error: AxiosError<ErrorResponse>) {
const message = error.response?.data?.message || 'An unexpected error occurred';
toast.error(message, {
toastId: 'generic-error',
});
return Promise.reject(error);
}Usage in Your App
// src/index.tsx or src/App.tsx
import { initializeInterceptors } from './api/axiosInstance';
// Initialize once at app startup
initializeInterceptors();
// Now use throughout your app
import api from './api/axiosInstance';
// All API calls get interceptors automatically
async function fetchUsers() {
const response = await api.get('/users');
return response.data;
}
async function createPost(data: any) {
const response = await api.post('/posts', data);
return response.data;
}// src/index.tsx or src/App.tsx
import { initializeInterceptors } from './api/axiosInstance';
// Initialize once at app startup
initializeInterceptors();
// Now use throughout your app
import api from './api/axiosInstance';
// All API calls get interceptors automatically
async function fetchUsers() {
const response = await api.get('/users');
return response.data;
}
async function createPost(data: any) {
const response = await api.post('/posts', data);
return response.data;
}When NOT to Use Interceptors
Don't use interceptors for:
- Request-specific logic — Handle in the individual API call
- Component-specific errors — Let components handle their own errors
- One-off headers — Pass them directly in the request
- Heavy computations — Keep interceptors fast
- External APIs — Create separate instances for different APIs
Best Practices Checklist
- Single source of truth — One interceptor file for entire app
- Environment-aware — Different behavior for dev/prod
- Performance monitoring — Track slow requests
- Proper error messages — User-friendly error text
- Token refresh logic — Handle 401s automatically
- Request cancellation — Clean up on unmount
- Rate limit handling — Respect API limits
- Retry with backoff — Don't hammer failing endpoints
- Request deduplication — Prevent duplicate calls
- Comprehensive logging — Debug issues quickly
The Bottom Line
Centralized axios interceptors eliminate repetitive code, reduce bugs, and make your API layer maintainable. Write auth, error handling, and retry logic once and it applies everywhere automatically. Stop copy-pasting headers. Stop handling 401s in every component. Start using interceptors.
What API challenges are you facing? Share in the comments.