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:
- Centralized Configuration: Define base URLs, default headers, and timeouts in one place.
- Shared Interceptors: Attach authentication headers, centralized logging, metrics tracking, and error handling across all requests.
- Connection Pooling: Reuse persistent TCP
connections using standard Node.js
http.Agentandhttps.Agentconfigurations.
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
- Always Set Timeouts: Never leave the
timeoutproperty empty; Node.js requests can hang indefinitely, exhausting sockets and memory. - Enable
keepAliveon HTTP Agents: Reusing sockets significantly reduces TCP handshake latency under heavy server load. - Avoid Dynamic Interceptor Registration: Add interceptors once during client initialization. Registering interceptors within route handlers causes memory leaks.
- 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.