Centralize Auth and Retry Logic with Axios Interceptors
Use Axios interceptors to inject bearer tokens and retry 401 responses in one place instead of repeating auth logic at every API call site.
18 Aug 2026, 01:11 UTC

Stop repeating auth headers and 401 logic
In a growing Node or browser codebase, API calls tend to look like this:
const res = await axios.get("/api/orders", {
headers: { Authorization: `Bearer ${getToken()}` },
});
When a token expires, every call site repeats the same status check:
if (res.status === 401) {
await refreshToken();
return axios.get("/api/orders", { ... });
}
Copying this pattern across modules means one change to the refresh flow forces edits in many files. It also makes it easy to forget a header on a new endpoint.
Useful takeaway: Create a single Axios instance and attach request and response interceptors. The request interceptor attaches the bearer token; the response interceptor detects 401, refreshes the token, and retries the original request. Call sites stay focused on business logic.
What an Axios interceptor actually does
An interceptor is a middleware function that sits between your application code and the underlying HTTP adapter. A request interceptor receives the Axios config object before the request is sent and can modify headers, URL, or data, or abort the request. A response interceptor receives the response object after the network call completes and can transform data or handle errors globally.
Worked example: centralized auth and retry
Assumptions: Axios v1.x, Node.js 18+, or a modern bundler in the browser. This example runs in Node.
Where to run: your project directory. Permissions: standard user account; no elevated privileges are needed.
import axios from "axios";
// Placeholder: replace with your actual token storage and refresh endpoint
const API_BASE_URL = "https://api.example.com";
const TOKEN_KEY = "access_token";
const api = axios.create({
baseURL: API_BASE_URL,
timeout: 10000,
});
// Request interceptor: inject bearer token on every outgoing request
api.interceptors.request.use((config) => {
const token = localStorage.getItem(TOKEN_KEY); // browser example
// In Node, read from process.env or a secure store instead
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
}, (error) => {
return Promise.reject(error);
});
// Response interceptor: handle 401 by refreshing and retrying once
let isRefreshing = false;
let refreshPromise = null;
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
if (!isRefreshing) {
isRefreshing = true;
refreshPromise = refreshAccessToken(); // placeholder function
}
try {
await refreshPromise;
originalRequest._retry = true;
return api(originalRequest);
} catch (refreshError) {
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
}
);
export default api;
Meaningful placeholders: API_BASE_URL, TOKEN_KEY, and refreshAccessToken(). Replace them with your environment's values before running.
Expected checks:
- Make a request with
api.get("/users"). - Confirm the
Authorizationheader appears in the request before the network call. - Force a 401 response and confirm the interceptor calls the refresh endpoint once, then retries the original request.
- Confirm the retry does not duplicate side effects for non-idempotent requests such as POST.
Risks: If the refresh endpoint also returns 401, the interceptor can loop until the process runs out of memory or the client times out. Guard the refresh with a flag and reject after a single failed attempt.
Trade-offs and limitations
- Order matters. Interceptors run in registration order. A later interceptor sees header changes made by an earlier one, so mutating
configcarelessly can corrupt state for downstream interceptors. - Debugging is harder. Errors surface inside interceptor chains, so stack traces may point to library code rather than your call site.
- Not every request should retry. Automatically retrying POST, PUT, or DELETE requests can create duplicate resources unless the endpoint is idempotent.
Practical verification
Add a temporary console.log(config.headers) inside the request interceptor, run the module, and make one request. If the header is logged before the response arrives, the interceptor is registered correctly. Remove the log before deploying.
Closing
Axios interceptors are a good fit when you need consistent auth headers and error handling across many endpoints. Keep the interceptor logic small, avoid mutating shared config beyond what is necessary, and treat automatic retries as an opt-in behavior per HTTP method.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.