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);
  }
);