Centralizing Auth and Error Handling in Axios with a Shared Instance
Create a single Axios instance with baseURL, timeout, and interceptors for auth and error normalization. Learn how to avoid duplication, manage interceptor order, and test the setup.
24 Dec 2025, 22:36 UTC

Why a Shared Axios Instance Matters
When a project grows, dozens of API calls scatter across the codebase. Repeating baseURL, timeout, and auth logic in every request is error‑prone and hard to maintain. A single, well‑configured Axios instance lets you declare those cross‑cutting concerns once and reuse them everywhere, keeping the rest of your code focused on business logic.
Setting Up the Client
Below is a minimal, TypeScript‑friendly configuration that demonstrates the core pattern. It uses axios.create to embed a baseURL and a timeout, then adds request and response interceptors for authentication and error normalization.
import axios, { AxiosInstance, AxiosRequestConfig, AxiosError } from "axios";
import { getAuthToken } from "./auth-store"; // runtime token provider
// 1. Create a shared client
export const apiClient: AxiosInstance = axios.create({
baseURL: process.env.API_BASE_URL, // e.g., "https://api.example.com/v1"
timeout: 10_000 // 10 seconds
});
// 2. Request interceptor: attach Authorization header
apiClient.interceptors.request.use(
(config: AxiosRequestConfig) => {
// guard against absolute URLs that bypass baseURL
if (!config.url?.startsWith("http")) {
const token = getAuthToken();
if (token) {
config.headers = config.headers ?? {};
config.headers["Authorization"] = `Bearer ${token}`;
}
}
return config;
},
(error) => Promise.reject(error)
);
// 3. Response interceptor: normalize error shape
apiClient.interceptors.response.use(
(response) => response,
(error: AxiosError) => {
const normalized = {
status: error.response?.status ?? null,
message: error.response?.data?.message ?? error.message,
details: error.response?.data ?? null
};
return Promise.reject(normalized);
}
);
Using the Client in Services
Inject the configured instance into your API service functions. This keeps the services testable and decoupled from the global Axios instance.
// user-service.ts
import { AxiosInstance } from "axios";
export function fetchUser(client: AxiosInstance, userId: string) {
return client.get(`/users/${userId}`).then((res) => res.data);
}
In production you call it with apiClient:
import { apiClient } from "./api-client";
import { fetchUser } from "./user-service";
fetchUser(apiClient, "12345").then(user => console.log(user)).catch(err => {
// err has the normalized shape from the interceptor
console.error(err.status, err.message);
});
Trade‑offs and Limitations
Interceptor Ordering
Axios request interceptors run in reverse registration order, while response interceptors run in the order they were added. If you add multiple interceptors (e.g., logging, retry), be mindful of this behavior. Verify the order against the installed version by printing the interceptor stack in a test script.
Global State Leakage
Interceptors should never mutate the shared config object directly. Clone any part you need to change to avoid leaking state between requests. For example, if you add a custom header, create a new headers object first.
Cancellation API Differences
Axios 1.x prefers AbortController via the signal option. The older CancelToken is still supported but is not recommended. Mixing them can cause subtle bugs; use one consistently or provide a shim if you need backward compatibility.
Non‑2xx Responses
By default Axios rejects promises for any HTTP status outside 200–299. If you rely on 4xx responses resolving (for example, to display validation errors without throwing), adjust validateStatus explicitly. Otherwise, handle all errors through the response interceptor’s normalized shape.
Hard‑coding Secrets
Never embed API keys or tokens in the client defaults. Read them from a secure runtime source (environment variables, secure vault, or a dedicated auth module) at request time.
Practical Checklist
- Confirm the Axios major version in
package.jsonand review release notes for interceptor ordering changes. - Run a quick script that logs request headers and response status to validate interceptor behavior.
- In unit tests, swap
apiClientforaxios-mock-adapterto assert that the Authorization header is present and that rejected errors match the normalized shape. - Test request cancellation by aborting an
AbortControllermid‑flight and ensuring the promise rejects with a cancellation error.
Bottom Line
Centralizing baseURL, timeout, authentication, and error handling in a shared Axios instance reduces duplication, enforces consistency, and simplifies testing. Just be aware of interceptor ordering, avoid mutating shared config, and choose a single cancellation strategy. With these practices in place, your API layer stays clean, maintainable, and resilient.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.