Centralizing Axios Configuration with Shared Instances and Interceptors
Repeating baseURL and auth headers on every Axios call creates drift and inconsistent error handling. A shared instance with interceptors centralizes that logic.
21 May 2026, 06:52 UTC

The problem with scattered Axios calls
When every API call repeats the same baseURL, timeout, and Authorization header, configuration drifts. One endpoint forgets the timeout, another hardcodes a token, and error handling becomes a patchwork of try/catch blocks that inspect error.response differently in each file. Mutating the global axios object is worse: the change leaks across every module in the process.
The practical decision is to create a dedicated instance with axios.create() and attach interceptors—functions that run before a request is sent or after a response is received. This isolates defaults per API surface and keeps authentication and error normalization in one place.
A shared instance with interceptors
Create a module that exports a configured client. The request interceptor injects credentials from a token store; the response interceptor unwraps successful payloads and maps failures into a consistent error shape.
// apiClient.js
import axios from 'axios';
const tokenStore = {
get: () => localStorage.getItem('authToken') // or secure storage in Node
};
const apiClient = axios.create({
baseURL: process.env.API_BASE_URL || 'https://api.example.com',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
});
apiClient.interceptors.request.use(
(config) => {
const token = tokenStore.get();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
apiClient.interceptors.response.use(
(response) => response.data,
(error) => {
if (error.response) {
const normalized = {
status: error.response.status,
code: error.response.data?.code || 'UNKNOWN',
message: error.response.data?.message || error.message,
original: error
};
return Promise.reject(normalized);
}
return Promise.reject({
status: 0,
code: 'NETWORK_ERROR',
message: error.message,
original: error
});
}
);
export default apiClient;
Read tokens from environment variables or secure storage inside the interceptor; never hardcode them in source. With this setup, callers receive response.data directly on success and a plain object with status, code, and message on failure. UI code no longer needs to know whether Axios wrapped the payload.
How interceptor ordering works
Interceptors run in registration order for requests and in reverse order for responses. If you add a logging interceptor after the auth interceptor, the logger sees the request after the token is attached. On the response side, the logger runs before the error normalizer, so it can inspect the raw Axios error before it is transformed.
This ordering matters when multiple interceptors mutate headers or payloads. Register the most specific transformation last for requests so it operates on the final config.
Limits and common mistakes
Version differences matter
Axios 0.x and 1.x differ in transform defaults, FormData serialization, and error object shape. Pin the major version in package.json and test against that version's changelog. An interceptor written for 0.x may assume properties that 1.x renamed or removed.
Do not swallow errors
A response interceptor that logs and returns undefined hides failures from callers. Always rethrow or return Promise.reject after logging. Similarly, a request interceptor that throws without returning a rejected promise breaks the chain; use return Promise.reject(error) or return the config.
validateStatus is not a universal fix
Setting validateStatus to accept 404 or 500 makes those responses resolve instead of reject. That is useful for endpoints where a 404 is a valid business outcome, but it risks silent failures for standard CRUD APIs. Keep the default 2xx-only behavior unless the API contract explicitly treats a non-2xx status as success.
responseType and parsing assumptions
Axios auto-transforms JSON responses, but responseType controls the output shape. A blob or arraybuffer response is not parsed into an object. If your interceptor assumes response.data is always an object, it will break for file downloads or text endpoints.
Cancellation in Axios 1.x
Prefer AbortController via the signal option. The older CancelToken API is still present in 1.x but is superseded and may be removed in future majors. Mixing the two can produce confusing abort states.
Runtime differences
Browser and Node adapters differ in capabilities such as streaming uploads and progress events. Verify interceptor behavior in each runtime you support.
Verifying the setup
To confirm the interceptor chain behaves as expected, run a small Node script against a local mock server. You will need Node.js and Axios installed in the project directory.
# Terminal: project root
node verify-client.js
The script should assert that a 500 response produces your normalized error object and that a 404 resolves or rejects according to your validateStatus choice. A minimal check looks like this:
// verify-client.js
import apiClient from './apiClient.js';
try {
await apiClient.get('/test-500');
} catch (err) {
console.assert(err.status === 500, 'Expected status 500');
console.assert(typeof err.message === 'string', 'Expected message string');
}
const ok = await apiClient.get('/test-200');
console.assert(ok.id !== undefined, 'Expected unwrapped data');
Run this against a local server that returns 200 and 500. If the assertions pass, the instance is isolating configuration and normalizing errors correctly. If not, check interceptor registration order and the installed Axios major version.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.