Dynamic Request Routing with Axios in Microservices
Dynamic request routing allows a microservices architecture to dynamically resolve target service endpoints at runtime based on context, load, tenant metadata, or service discovery registries. This article explains how to implement dynamic request routing using the Axios HTTP client by leveraging custom Axios instances, request interceptors, service discovery integration, and runtime URL rewriting.
1. Create a Base Axios Instance
Start by creating a centralized Axios instance. This instance acts as the gateway for your outgoing service-to-service communications, allowing you to attach middleware logic without modifying individual API call sites.
const axios = require('axios');
const serviceClient = axios.create({
timeout: 5000,
headers: {
'Content-Type': 'application/json'
}
});2. Implement Dynamic URL Resolution Using Interceptors
Axios interceptors can intercept requests before they are sent. By
attaching a request interceptor, you can inspect custom headers, request
metadata, or virtual service hostnames, and dynamically rewrite the
baseURL or url property.
// A simple service registry map (can be populated dynamically via Consul, Eureka, or DNS)
const serviceRegistry = {
'user-service': 'http://10.0.1.10:8081',
'order-service': 'http://10.0.1.11:8082',
'payment-service': 'http://10.0.1.12:8083'
};
serviceClient.interceptors.request.use(async (config) => {
// Extract target service identifier from custom config or URL prefix
const targetService = config.headers['X-Target-Service'] || config.serviceName;
if (targetService && serviceRegistry[targetService]) {
// Dynamically override the baseURL to point to the resolved microservice instance
config.baseURL = serviceRegistry[targetService];
} else if (!config.baseURL && !config.url.startsWith('http')) {
throw new Error(`Routing failed: Target service "${targetService}" not found.`);
}
// Optional: Dynamic tenant-based or regional routing
const region = config.headers['X-Region'];
if (region === 'eu-west') {
config.baseURL = config.baseURL.replace('.com', '.eu');
}
return config;
}, (error) => {
return Promise.reject(error);
});3. Integrating with Dynamic Service Discovery
In scalable production environments, hardcoded registries are insufficient. You can integrate dynamic resolution using discovery tools (like Consul, Eureka, or Kubernetes DNS):
async function resolveServiceAddress(serviceName) {
// Query your dynamic service registry or service mesh
// const instances = await consul.health.service(serviceName);
// Return the best available instance based on load-balancing logic
return 'http://dynamic-node-ip:port';
}
serviceClient.interceptors.request.use(async (config) => {
if (config.serviceName) {
const resolvedUrl = await resolveServiceAddress(config.serviceName);
config.baseURL = resolvedUrl;
}
return config;
});4. Dispatching Routed Requests
When making calls from your application logic, specify the target service name using custom request configuration rather than hardcoding static hostnames:
async function fetchUserProfile(userId) {
return serviceClient.get(`/users/${userId}`, {
serviceName: 'user-service',
headers: {
'X-Region': 'us-east'
}
});
}5. Handling Dynamic Failovers with Response Interceptors
To complete dynamic routing, implement response interceptors to
handle endpoint failures. If a node fails or returns a
503 Service Unavailable, the interceptor can reroute the
request to an alternative healthy node:
serviceClient.interceptors.response.use(
(response) => response,
async (error) => {
const { config } = error;
// Retry only once on service failure
if (!config || config._retry) {
return Promise.reject(error);
}
if (error.response && error.response.status === 503) {
config._retry = true;
// Resolve a fallback instance
config.baseURL = 'http://fallback-service-node:8080';
return serviceClient(config);
}
return Promise.reject(error);
}
);