Normalize Data with Axios Response Interceptors

This article explains how to leverage Axios response interceptors to automatically normalize API response data before it reaches your application logic. By attaching a transformation layer directly to an Axios instance, you can seamlessly convert key formats (such as snake_case to camelCase), flatten nested structures, or reshape relational entities across all incoming HTTP requests without repetitive code in your services.

Understanding Axios Interceptors

Axios interceptors are middleware functions that you can attach to an Axios instance to inspect or modify HTTP requests before they are sent, or HTTP responses before they are handled by then or catch blocks.

The axios.interceptors.response.use() method accepts two callback functions:

  1. An onFulfilled handler that receives the response object when a request succeeds (status code in the 2xx range).
  2. An onRejected handler that receives the error object when a request fails.

Creating a Normalization Function

Data normalization involves standardizing incoming payloads to match your frontend data structures. A common use case is transforming backend snake_case properties into frontend-friendly camelCase properties.

// Utility function to convert snake_case keys to camelCase recursively
function toCamelCase(str) {
  return str.replace(/([-_][a-z])/gi, ($1) =>
    $1.toUpperCase().replace('-', '').replace('_', '')
  );
}

function normalizeKeys(data) {
  if (Array.isArray(data)) {
    return data.map((item) => normalizeKeys(item));
  }
  if (data !== null && typeof data === 'object' && !(data instanceof Date)) {
    return Object.keys(data).reduce((acc, key) => {
      const camelKey = toCamelCase(key);
      acc[camelKey] = normalizeKeys(data[key]);
      return acc;
    }, {});
  }
  return data;
}

Attaching the Normalization Interceptor

Create a dedicated Axios instance and register the normalization function inside the response interceptor. Modify response.data and return the modified response object.

import axios from 'axios';

// 1. Create a custom Axios instance
const apiClient = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  },
});

// 2. Add the response interceptor
apiClient.interceptors.response.use(
  (response) => {
    // Check if the response contains data and normalize it
    if (response.data) {
      response.data = normalizeKeys(response.data);
    }
    return response;
  },
  (error) => {
    // Optionally normalize error response bodies as well
    if (error.response && error.response.data) {
      error.response.data = normalizeKeys(error.response.data);
    }
    return Promise.reject(error);
  }
);

export default apiClient;

Consuming Normalized Data

When using the configured apiClient, components and services receive the normalized data automatically:

import apiClient from './apiClient';

async function fetchUserProfile(userId) {
  // Backend returns: { user_id: 101, first_name: "Jane", created_at: "2024-01-01" }
  const response = await apiClient.get(`/users/${userId}`);
  
  // Normalized output: { userId: 101, firstName: "Jane", createdAt: "2024-01-01" }
  return response.data;
}

Best Practices for Interceptor Normalization