Chaining Axios Interceptors for Request Pipelines

Axios interceptors allow developers to intercept HTTP requests and responses before they are handled by then or catch. By chaining multiple interceptors, you can construct a modular, multi-stage processing pipeline where discrete tasks—such as request tracing, authentication token injection, payload encryption, and centralized error normalization—are executed in an isolated, sequential manner. This guide explains how request and response interceptors execute, how to structure multi-stage pipelines, and how to implement them cleanly using custom Axios instances.


How Axios Interceptor Chaining Works

Axios maintains internal arrays for both request and response interceptors. When a request is dispatched, Axios constructs a promise chain:

  1. Request Interceptors: Execute in reverse order of registration (Last-In, First-Out). The interceptor added last runs first.
  2. HTTP Dispatch: The actual network call takes place.
  3. Response Interceptors: Execute in the exact order of registration (First-In, First-Out). The interceptor added first runs first.
[Request Interceptor 2] -> [Request Interceptor 1] -> [Network Request] -> [Response Interceptor 1] -> [Response Interceptor 2]

To control the execution flow precisely, create an isolated Axios instance rather than mutating the global Axios object.


Step 1: Create an Axios Instance

Create a dedicated client instance to ensure interceptors do not conflict across different API integrations:

import axios from 'axios';

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

Step 2: Define Modular Interceptor Functions

Keep each pipeline stage focused on a single responsibility.

Request Stages

// Stage A: Add Correlation ID / Tracing
export const traceInterceptor = (config) => {
  config.headers['X-Correlation-ID'] = crypto.randomUUID();
  config.metadata = { startTime: Date.now() };
  return config;
};

// Stage B: Authentication Token Injection
export const authInterceptor = async (config) => {
  const token = localStorage.getItem('access_token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
};

// Stage C: Payload Formatting / Sanitization
export const sanitizeInterceptor = (config) => {
  if (config.data && typeof config.data === 'object') {
    // Example: Strip empty string fields
    Object.keys(config.data).forEach((key) => {
      if (config.data[key] === '') {
        delete config.data[key];
      }
    });
  }
  return config;
};

Response Stages

// Stage A: Performance Logging
export const metricsInterceptor = (response) => {
  const duration = Date.now() - response.config.metadata.startTime;
  console.log(`[HTTP] ${response.config.url} completed in ${duration}ms`);
  return response;
};

// Stage B: Data Normalization
export const unwrapDataInterceptor = (response) => {
  // Return the data payload directly instead of the full Axios response object
  return response.data;
};

// Stage C: Centralized Error Handler
export const errorNormalizationInterceptor = (error) => {
  const normalizedError = {
    status: error.response?.status || 500,
    message: error.response?.data?.message || error.message || 'An unexpected error occurred.',
    timestamp: new Date().toISOString(),
  };

  return Promise.reject(normalizedError);
};

Step 3: Register and Order the Pipeline

Because request interceptors execute in reverse registration order, attach them from the innermost (closest to network) to outermost (first to run).

// REQUEST PIPELINE
// Desired execution order: traceInterceptor -> authInterceptor -> sanitizeInterceptor

apiClient.interceptors.request.use(sanitizeInterceptor); // Runs 3rd
apiClient.interceptors.request.use(authInterceptor);     // Runs 2nd
apiClient.interceptors.request.use(traceInterceptor);    // Runs 1st

// RESPONSE PIPELINE
// Desired execution order: metricsInterceptor -> unwrapDataInterceptor

apiClient.interceptors.response.use(
  metricsInterceptor,
  errorNormalizationInterceptor
); // Runs 1st

apiClient.interceptors.response.use(
  unwrapDataInterceptor,
  errorNormalizationInterceptor
); // Runs 2nd

Step 4: Manage and Eject Interceptors Dynamically

When implementing conditional stages (such as temporary retry logic or debug logging), store the interceptor reference ID to remove it from the pipeline when no longer needed:

// Register a debug interceptor
const debugInterceptorId = apiClient.interceptors.request.use((config) => {
  console.debug('Outgoing payload:', config.data);
  return config;
});

// Eject the interceptor when done
apiClient.interceptors.request.eject(debugInterceptorId);

Best Practices for Multi-Stage Pipelines