How to Build an In-Memory Cache Axios Adapter

This article demonstrates how to create a custom Axios adapter that intercepts outgoing HTTP requests to serve cached responses from memory, reducing redundant network calls and improving application performance. You will learn the mechanics of Axios adapters, how to construct a cache key, manage in-memory storage with Time-To-Live (TTL) expiration, and bind the custom adapter to an Axios instance.


Understanding Axios Adapters

Axios delegates the actual dispatching of requests to adapters. By default, it uses the xhr (browser) or http (Node.js) adapter. By creating a wrapper around the default adapter, you can inspect the request configuration before it hits the network, return a cached response if available, or execute the network call and store the result for future use.

Step 1: Create the Cache Store

A basic in-memory cache can be implemented using a JavaScript Map. To prevent stale data, entries should store the response data along with a timestamp to support TTL-based expiration.

class MemoryCache {
  constructor(defaultTTL = 60000) { // Default TTL: 60 seconds
    this.cache = new Map();
    this.defaultTTL = defaultTTL;
  }

  generateKey(config) {
    const { method, url, params, data } = config;
    return `${method?.toUpperCase()}:${url}:${JSON.stringify(params)}:${JSON.stringify(data)}`;
  }

  get(key) {
    const entry = this.cache.get(key);
    if (!entry) return null;

    const isExpired = Date.now() > entry.expiry;
    if (isExpired) {
      this.cache.delete(key);
      return null;
    }

    return entry.response;
  }

  set(key, response, ttl = this.defaultTTL) {
    this.cache.set(key, {
      response,
      expiry: Date.now() + ttl,
    });
  }

  clear() {
    this.cache.clear();
  }
}

Step 2: Implement the Custom Adapter

An Axios adapter is a function that receives the request config object and returns a Promise resolving to an Axios response structure.

import axios from 'axios';

export function createCacheAdapter(options = {}) {
  const cache = new MemoryCache(options.ttl);
  // Retrieve the default adapter for the current environment
  const defaultAdapter = axios.getAdapter(axios.defaults.adapter);

  return async function cacheAdapter(config) {
    // Only cache read operations (GET, HEAD, OPTIONS) by default
    const isCacheable = (config.method || 'get').toLowerCase() === 'get';

    if (!isCacheable || config.cache === false) {
      return defaultAdapter(config);
    }

    const cacheKey = cache.generateKey(config);
    const cachedResponse = cache.get(cacheKey);

    if (cachedResponse) {
      return {
        ...cachedResponse,
        config,
        request: { fromCache: true }
      };
    }

    // Cache miss: execute network request via default adapter
    const response = await defaultAdapter(config);

    // Only cache successful 2xx responses
    if (response.status >= 200 && response.status < 300) {
      cache.set(cacheKey, {
        data: response.data,
        status: response.status,
        statusText: response.statusText,
        headers: response.headers,
      });
    }

    return response;
  };
}

Step 3: Apply the Adapter to Axios

Instantiate an Axios client and pass the custom adapter through the configuration object.

// Initialize the custom adapter with a 30-second TTL
const cacheAdapter = createCacheAdapter({ ttl: 30000 });

// Create an Axios instance
const apiClient = axios.create({
  baseURL: 'https://api.example.com',
  adapter: cacheAdapter,
});

// Usage
async function fetchData() {
  // First call: Triggers network request and caches the result
  const response1 = await apiClient.get('/users');
  console.log('From network:', response1.data);

  // Second call: Returned immediately from in-memory cache
  const response2 = await apiClient.get('/users');
  console.log('From cache:', response2.data);

  // Bypass cache explicitly when needed
  const response3 = await apiClient.get('/users', { cache: false });
  console.log('Bypassed cache:', response3.data);
}

Key Considerations