Axios Singleton Pattern in Server-Side JavaScript

This guide explains how to design and implement a centralized Axios HTTP client singleton in a server-side JavaScript (Node.js) architecture. It covers why the singleton pattern is beneficial for backend API interactions, how to leverage module caching for single-instance lifecycle management, and how to configure global interceptors, timeouts, and connection pooling for scalable, production-grade applications.


Why Use a Singleton Pattern for Axios?

In a server-side architecture, multiple services, controllers, or repositories often need to communicate with external APIs or microservices. Instantiating a new axios.create() instance inside every function or module creates unnecessary overhead and leads to fragmented configuration.

Using a singleton pattern provides:


Core Implementation: ES Module / CommonJS Singleton

Node.js naturally caches modules after their first load (require or import). You can leverage this built-in behavior to create a functional singleton.

apiClient.js

import axios from 'axios';
import http from 'http';
import https from 'https';

// Configure connection pooling
const httpAgent = new http.Agent({ keepAlive: true });
const httpsAgent = new https.Agent({ keepAlive: true });

// Create the single Axios instance
const apiClient = axios.create({
  baseURL: process.env.API_BASE_URL || 'https://api.example.com',
  timeout: 10000,
  httpAgent,
  httpsAgent,
  headers: {
    'Content-Type': 'application/json',
    Accept: 'application/json',
  },
});

// Request Interceptor: Attach authentication or tracing headers
apiClient.interceptors.request.use(
  (config) => {
    // Example: Inject server-to-server token or correlation ID
    const correlationId = config.headers['x-correlation-id'] || 'default-trace-id';
    config.headers['x-correlation-id'] = correlationId;
    return config;
  },
  (error) => Promise.reject(error)
);

// Response Interceptor: Centralize error formatting and logging
apiClient.interceptors.response.use(
  (response) => response.data, // Directly return response data
  (error) => {
    const customError = {
      message: error.response?.data?.message || error.message || 'HTTP Request Failed',
      status: error.response?.status || 500,
      data: error.response?.data || null,
    };
    return Promise.reject(customError);
  }
);

// Export the initialized instance directly
export default apiClient;

Object-Oriented Singleton Implementation

For complex architectures that require dynamic initialization, multi-tenant switching, or custom wrapper methods, a class-based singleton pattern is ideal.

HttpClient.js

import axios from 'axios';
import http from 'http';
import https from 'https';

class HttpClient {
  constructor() {
    if (HttpClient.instance) {
      return HttpClient.instance;
    }

    this.client = axios.create({
      baseURL: process.env.API_BASE_URL,
      timeout: 15000,
      httpAgent: new http.Agent({ keepAlive: true }),
      httpsAgent: new https.Agent({ keepAlive: true }),
    });

    this._initializeInterceptors();
    HttpClient.instance = this;
  }

  _initializeInterceptors() {
    this.client.interceptors.request.use(
      (config) => {
        // Global pre-flight transformations
        return config;
      },
      (error) => Promise.reject(error)
    );

    this.client.interceptors.response.use(
      (response) => response,
      (error) => {
        // Global error logging
        console.error(`[HTTP Error] ${error.config?.url}:`, error.message);
        return Promise.reject(error);
      }
    );
  }

  // Wrapper methods for standardized usage
  get(url, config = {}) {
    return this.client.get(url, config);
  }

  post(url, data = {}, config = {}) {
    return this.client.post(url, data, config);
  }

  put(url, data = {}, config = {}) {
    return this.client.put(url, data, config);
  }

  delete(url, config = {}) {
    return this.client.delete(url, config);
  }
}

// Freeze the instance to prevent modifications
const instance = new HttpClient();
Object.freeze(instance);

export default instance;

Using the Singleton Across the Application

Consume the singleton instance inside domain services or controller layers without re-initializing network settings.

userService.js

import httpClient from './HttpClient.js';

export class UserService {
  static async getUserById(userId) {
    try {
      const response = await httpClient.get(`/users/${userId}`);
      return response.data;
    } catch (error) {
      throw new Error(`Failed to retrieve user: ${error.message}`);
    }
  }

  static async createUser(userData) {
    const response = await httpClient.post('/users', userData);
    return response.data;
  }
}

Key Best Practices for Server-Side Axios Singletons

  1. Always Set Timeouts: Never leave the timeout property empty; Node.js requests can hang indefinitely, exhausting sockets and memory.
  2. Enable keepAlive on HTTP Agents: Reusing sockets significantly reduces TCP handshake latency under heavy server load.
  3. Avoid Dynamic Interceptor Registration: Add interceptors once during client initialization. Registering interceptors within route handlers causes memory leaks.
  4. Isolate Specific Services: If communicating with multiple distinct third-party APIs (e.g., Stripe and Twilio), create a dedicated singleton file for each external provider to prevent cross-configuration leakage.