How to Build and Use Custom Axios Adapters

Axios is a versatile HTTP client for JavaScript, primarily designed to run in both Node.js (via http) and browser environments (via XMLHttpRequest). By building custom adapters, developers can replace this default transport layer with custom logic, such as using the Fetch API, routing requests through Web Workers, mocking responses for testing, or adding specialized caching mechanisms. This article explains the structure of an Axios adapter, demonstrates how to write a custom implementation, and covers how to register it globally or per instance.


Understanding the Axios Adapter Interface

An adapter in Axios is a function that receives a configuration object (config) and returns a Promise that resolves to a standard Axios response object or rejects with an Axios error.

The returned response object must follow this structure:

{
  data: {},        // The response body
  status: 200,     // HTTP status code
  statusText: 'OK',// HTTP status message
  headers: {},     // Response headers object
  config: config,  // The original request configuration
  request: {}      // The native request object (optional)
}

If the request fails, the adapter should reject with an Error object containing the same config, request, response (if available), and an appropriate error code.


Step 1: Writing a Custom Adapter

Below is an example of a custom adapter implemented using the modern native fetch API:

// fetchAdapter.js
export function fetchAdapter(config) {
  return new Promise((resolve, reject) => {
    // 1. Build request URL and parameters
    const url = new URL(config.url, config.baseURL);
    if (config.params) {
      Object.keys(config.params).forEach((key) =>
        url.searchParams.append(key, config.params[key])
      );
    }

    // 2. Prepare request options
    const options = {
      method: config.method ? config.method.toUpperCase() : 'GET',
      headers: config.headers || {},
      body: config.data,
      signal: config.signal, // Support cancellation
    };

    // 3. Execute request
    fetch(url.toString(), options)
      .then(async (response) => {
        // Parse data based on responseType (defaulting to JSON)
        let responseData;
        if (config.responseType === 'text') {
          responseData = await response.text();
        } else if (config.responseType === 'blob') {
          responseData = await response.blob();
        } else {
          responseData = await response.json().catch(() => null);
        }

        // Convert Headers to a plain object
        const responseHeaders = {};
        response.headers.forEach((value, key) => {
          responseHeaders[key] = value;
        });

        const axiosResponse = {
          data: responseData,
          status: response.status,
          statusText: response.statusText,
          headers: responseHeaders,
          config: config,
          request: null
        };

        // Resolve or reject based on validateStatus
        const validateStatus = config.validateStatus || ((status) => status >= 200 && status < 300);
        
        if (validateStatus(response.status)) {
          resolve(axiosResponse);
        } else {
          const error = new Error(`Request failed with status code ${response.status}`);
          error.config = config;
          error.response = axiosResponse;
          reject(error);
        }
      })
      .catch((error) => {
        // Handle network errors or aborted requests
        error.config = config;
        reject(error);
      });
  });
}

Step 2: Plugging the Custom Adapter into Axios

You can apply custom adapters at three different levels depending on your use case.

Attach the adapter to an Axios instance created using axios.create():

import axios from 'axios';
import { fetchAdapter } from './fetchAdapter';

const apiClient = axios.create({
  baseURL: 'https://api.example.com',
  adapter: fetchAdapter,
});

apiClient.get('/users')
  .then((response) => console.log(response.data))
  .catch((error) => console.error(error));

2. Globally

Override the default adapter for all standard axios requests across the application:

import axios from 'axios';
import { fetchAdapter } from './fetchAdapter';

axios.defaults.adapter = fetchAdapter;

axios.get('https://api.example.com/data')
  .then((response) => console.log(response.data));

3. Per-Request

Specify an adapter for an individual call without modifying global or instance settings:

import axios from 'axios';
import { fetchAdapter } from './fetchAdapter';

axios.get('https://api.example.com/data', {
  adapter: fetchAdapter
});

Step 3: Supporting Adapter Chaining (Axios v1+)

Axios allows specifying an array of adapters. Axios iterates through the array and uses the first adapter that executes successfully or returns a valid response handler:

import axios from 'axios';
import { fetchAdapter } from './fetchAdapter';

const client = axios.create({
  adapter: [fetchAdapter, 'xhr', 'http']
});

In this setup, Axios attempts to use fetchAdapter. If it is unavailable or throws a condition indicating it cannot handle the request, Axios falls back to the built-in browser (xhr) or Node.js (http) adapter.