All You Need to Know About Axios and Interceptors

Axios is one of the most popular HTTP client libraries in the JavaScript ecosystem. While the native Fetch API has become more powerful, Axios continues to offer a rich feature set that simplifies common HTTP tasks. One of its most powerful features is interceptors, which allow us to intercept and modify requests and responses before they reach our application code.
This article explores Axios interceptors in depth, from basic concepts to advanced implementations using popular interceptor libraries. By the end, we'll understand how to leverage interceptors to add logging, automatic retries, authentication, and caching to our HTTP layer.
Express API Server
First, let's create a realistic API server that simulates various scenarios to test our interceptors. Run the command npm init and create the server.js file with the following content:
app.get('/api/success', (req, res) => {
res.json({
message: 'OK',
timestamp: new Date().toISOString()
});
});
app.get('/api/fail', (req, res) => {
res.status(500).json({ error: 'NO OK' });
});
app.get('/api/delay', async (req, res) => {
const seconds = parseInt(req.query.seconds) || 1;
const delay = seconds * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
res.json({
message: 'OK'
});
});
app.get('/api/random-fail', (req, res) => {
const failureRate = parseInt(req.query.percentage) || 50;
const randomValue = Math.random() * 100;
if (randomValue < failureRate) {
res.status(500).json({
error: 'NO OK',
});
} else {
res.json({
message: 'OK',
timestamp: new Date().toISOString()
});
}
});
app.get('/api/protected', (req, res) => {
const header = req.headers.authorization;
if (!header) {
return res.status(401).json({ error: 'Unauthorized' });
}
const token = header.replace('Bearer ', '');
if (token !== 'ABC') {
return res.status(401).json({ error: 'Unauthorized' });
}
res.json({
message: 'OK',
timestamp: new Date().toISOString()
});
});
We can start the server at any time by running the command node server.js.
What is Axios?
Axios is a promise-based HTTP client for the browser and Node.js. It provides a simple and intuitive API for making HTTP requests with built-in support for features that would require additional code with native solutions. Axios provides features like:
Promise-based API: Clean async/await syntax support
Automatic JSON transformation: Requests and responses are automatically converted
Request/Response interceptors: Modify requests or responses globally
Request cancellation: Built-in support for aborting requests
Timeout configuration: Set time limits for requests
And more…
Install Axios by running the command:
npm install axios
Here's a simple example that demonstrates the basics of Axios:
import axios from 'axios';
const client = axios.create({
baseURL: 'http://localhost:3000'
});
async function run() {
const response = await client.get('/api/success');
console.log(response.data);
}
run();
Axios offers several configuration options, which we can find here. The most commonly used Axios parameters include:
url: The server URL (required for all requests).method: HTTP method (defaults toget).baseURL: Base URL for relative URLs (commonly set for API clients).data: Request body data forPOST,PUT, andPATCH.params: URL query parameters.headers: Custom headers (frequently used for auth, content-type).auth: HTTP Basic authentication.timeout: Request timeout in milliseconds.responseType: Expected response format (json,text, etc.).validateStatus: Custom status code validation.
Axios vs Fetch API
Understanding the differences between Axios and the native Fetch API helps us make informed decisions about which tool to use.
Fetch API
The Fetch API is the native JavaScript solution for making HTTP requests, available in modern browsers and Node.js (v18+).
Pros:
No dependencies: Built into the platform, no installation required.
Smaller bundle size: No additional library weight.
Modern standard: Part of the web platform standard.
Stream support: Native support for readable streams.
Cons:
No timeout support: Requires manual implementation with AbortController.
Manual JSON parsing: Must call
.json()on responses.No automatic error handling: Network errors only; HTTP errors (4xx, 5xx) don't reject.
Verbose error checking: Must check the
response.okmanually.No interceptors: Requires wrapper functions for global behavior.
No upload progress: Cannot track upload progress easily.
Axios
Pros:
Built-in timeout: Simple timeout configuration.
Automatic JSON handling: Automatic request/response transformation.
Better error handling: HTTP errors automatically reject promises.
Interceptors: Powerful request/response modification.
Request cancellation: Clean API for aborting requests.
Progress tracking: Built-in upload/download progress events.
Backward compatibility: Works in older browsers with polyfills.
Cons:
External dependency: Adds ~13KB to our bundle (minified).
Additional maintenance: Depends on third-party library updates.
Learning curve: Additional API concepts to understand.
Common Use Cases
Choose Axios when: We need interceptors, automatic error handling, timeout support, or are building a complex application with consistent HTTP patterns.
Choose Fetch when: We want zero dependencies, need advanced streaming capabilities, or are building a simple application with minimal HTTP requirements.
Interceptors
Interceptors are functions that Axios calls for every request or response. They allow us to modify requests before they're sent or process responses before they reach our application code. Think of them as middleware for our HTTP client.
Types of Interceptors
Request Interceptors: Execute before a request is sent to the server. Common uses include:
Adding authentication tokens
Logging requests
Modifying headers
Transforming request data
Response Interceptors: Execute after a response is received but before it reaches our application. Common uses include:
Handling errors globally
Logging responses
Caching responses
Here's a basic example demonstrating both request and response interceptors:
import axios from 'axios';
const client = axios.create({
baseURL: 'http://localhost:3000'
});
client.interceptors.request.use(
(config) => {
console.log('Request:', config.method.toUpperCase(), config.url);
config.metadata = { startTime: Date.now() };
return config;
},
(error) => {
console.error('Request error:', error);
return Promise.reject(error);
}
);
client.interceptors.response.use(
(response) => {
const duration = Date.now() - response.config.metadata.startTime;
console.log('Response:', response.status, `(${duration}ms)`);
return response;
},
(error) => {
if (error.response) {
console.error('Response error:', error.response.status);
} else if (error.request) {
console.error('No response received');
} else {
console.error('Request setup error:', error.message);
}
return Promise.reject(error);
}
);
run();
In this example, the request interceptor logs outgoing requests and adds metadata for timing. The response interceptor calculates request duration and handles errors consistently across the application.
Interceptor Execution Order
Understanding how interceptors execute is crucial for implementing complex behaviors. Axios processes interceptors in a specific order that resembles a middleware chain.
Request Interceptor Order
Request interceptors execute in reverse order of registration. The last registered interceptor runs first. This reverse order means that interceptors registered later have priority and can modify the configuration before earlier interceptors see it. This is useful when we want to override default behaviors.
Response Interceptor Order
Response interceptors are executed in the order of registration. The first registered interceptor runs first.
import axios from 'axios';
const client = axios.create({
baseURL: 'http://localhost:3000'
});
client.interceptors.request.use((config)=>{
console.info('logging interceptor');
return config;
}, (error) => Promise.reject(error));
client.interceptors.request.use((config)=>{
console.info('Request interceptor 2');
return config;
}, (error) => Promise.reject(error));
client.interceptors.request.use((config)=>{
console.info('Request interceptor 3');
return config;
}, (error) => Promise.reject(error));
client.interceptors.response.use((response)=>{
console.info('Response interceptor 1');
return response;
}, (error) => Promise.reject(error));
client.interceptors.response.use((response)=>{
console.info('Response interceptor 2');
return response;
}, (error) => Promise.reject(error));
client.interceptors.response.use((response)=>{
console.info('Response interceptor 3');
return response;
}, (error) => Promise.reject(error));
async function run() {
const response = await client.get('/api/success');
console.log(response.data);
}
run();
In the example above, the request/response cycle follows this pattern:
Request Interceptor 3 (last registered)
↓
Request Interceptor 2
↓
Request Interceptor 1 (first registered)
↓
HTTP Request sent to server
↓
HTTP Response received from server
↓
Response Interceptor 1 (first registered)
↓
Response Interceptor 2
↓
Response Interceptor 3 (last registered)
↓
Response returned to application
Understanding this order is essential when combining multiple interceptors:
Authentication should be added early in the request chain: Register auth interceptors(request) last so they run first.
Retry logic needs an early position in the response chain: Register retry interceptors(response) first, so they handle errors before other error handlers.
Caching should happen late in the response chain: Register cache interceptors(response) last so they receive fully processed responses.
Logging should generally ocurr at both ends: Register loggers first for requests (so they run last) and first for responses (so they run first) to capture the final state.
Popular Interceptor Libraries
Logging: axios-logger
The axios-logger library automatically logs information about every HTTP request and response. When we add it to our Axios instance, it intercepts both requests and responses to provide detailed debugging information. Use it only in development. Install it by running the command npm install axios-logger.
import axios from 'axios';
import * as AxiosLogger from 'axios-logger';
const client = axios.create({
baseURL: 'http://localhost:3000'
});
client.interceptors.request.use(
(request) => AxiosLogger.requestLogger(request, {
prefixText: 'API',
}),
(error) => AxiosLogger.errorLogger(error, {
prefixText: 'API',
logger: console.error
})
);
client.interceptors.response.use(
(response) => AxiosLogger.responseLogger(response, {
prefixText: 'API',
}),
(error) => AxiosLogger.errorLogger(error, {
prefixText: 'API',
logger: console.error
})
);
async function run() {
await client.get('/api/success');
try {
await client.get('/api/fail');
} catch (error) {
}
}
run();
We can configure the following parameters in axios-logger to control what information is logged and how it's formatted:
method: Include HTTP method. The default value istrue.url: Include request URL. The default value istrue.params: Include URL query parameters. The default value isfalse.data: Include request/response body data. The default value istrue.status: Include HTTP status code. The default value istrue.statusText: Include HTTP status text. The default value istrue.headers: Include HTTP headers. The default value isfalse.prefixText: Custom prefix text orfalseto disable. The default value isAxios.dateFormat: Timestamp format orfalseto disable. The default value isfalse.logger: Custom logger function. The default value isconsole.log.
Security: axios-token-interceptor
The axios-token-interceptor library simplifies adding authentication tokens to requests. It handles token storage, cache handling, and automatic injection. Install it by running the command npm install axios-token-interceptor.
import axios from 'axios';
import tokenProvider from 'axios-token-interceptor';
const client = axios.create({
baseURL: 'http://localhost:3000'
});
const cache = tokenProvider.tokenCache(
() => Promise.resolve("ABC"),
{ maxAge: 3600000 }
);
client.interceptors.request.use(
tokenProvider({
getToken: cache
})
);
async function run() {
const response = await client.get('/api/protected');
console.log(response.data);
}
run();
We can configure parameters for two main functions in the axios-token-interceptor library:
Token Provider Parameters
token: Static token string for all requests.getToken: Function that returns a token (sync or async).header: HTTP header name for token injection. The default value isAuthorization.headerFormatter: Function to format the header value. The default value is(token) => 'Bearer ${token}'.
Either token or getToken must be provided.
Token Cache Parameters
maxAge: Fixed cache duration in milliseconds.getMaxAge: Function to compute cache duration from token.
Either maxAge or getMaxAge should be provided for effective caching.
Caching: axios-cache-interceptor
The axios-cache-interceptor library adds sophisticated HTTP caching to Axios, dramatically reducing redundant network requests and improving application performance. Install it by running the command npm install axios-cache-interceptor.
import axios from 'axios';
import { setupCache } from 'axios-cache-interceptor';
const client = axios.create({
baseURL: 'http://localhost:3000'
});
const clientWithCache = setupCache(client, {
ttl: 15 * 60 * 1000
});
async function run() {
const response = await clientWithCache.get('/api/success');
console.log(response.data);
const response2 = await clientWithCache.get('/api/success');
console.log(response2.data);
}
run();
The axios-cache-interceptor uses both a request interceptor and a response interceptor internally:
The request interceptor runs before requests are sent to the network and is responsible for:
Generating cache keys for requests.
Checking if valid cached responses exist.
Serving cached responses when available.
Handling concurrent requests for the same resource.
Forwarding requests to the network when the cache is missing or stale.
The response interceptor (
defaultResponseInterceptor) runs after responses are received from the network and handles:Determining if responses should be cached.
Interpreting HTTP cache headers for TTL calculation.
Storing valid responses in cache.
Resolving pending concurrent requests.
Handling errors with stale cache fallback.
We can configure both global options when setting up the cache interceptor and per-request options for individual HTTP requests. Remember, we can set all per-request options as global defaults in setupCache().
Resilience: axios-retry
The axios-retry package adds intelligent retry logic to our Axios instance, automatically retrying failed requests based on configurable conditions. Install it by running the command npm install axios-retry.
import axios from 'axios';
import axiosRetry from 'axios-retry';
const client = axios.create({
baseURL: 'http://localhost:3000'
});
client.interceptors.request.use(function (config) {
console.log('Request interceptor registered before');
return config;
}, function (error) {
console.log('Request error interceptor registered before');
return Promise.reject(error);
});
client.interceptors.response.use(function (response) {
console.log('Response interceptor registered before');
return response;
}, function (error) {
console.log('Response error interceptor registered before');
return Promise.reject(error);
});
axiosRetry(client, {
retries: 3,
retryDelay: axiosRetry.exponentialDelay,
retryCondition: (error) => {
return axiosRetry.isNetworkOrIdempotentRequestError(error);
},
onRetry: (retryCount, error, requestConfig) => {
console.log(`Retry attempt #${retryCount}`);
console.log(`Error: ${error.message}`);
}
});
client.interceptors.request.use(function (config) {
console.log('Request interceptor registered after');
return config;
}, function (error) {
console.log('Request error interceptor registered after');
return Promise.reject(error);
});
client.interceptors.response.use(function (response) {
console.log('Response interceptor registered after');
return response;
}, function (error) {
console.log('Response error interceptor registered after');
return Promise.reject(error);
});
async function run() {
try {
const response = await client.get('/api/random-fail?percentage=90');
console.log(response.data);
} catch (error) {
console.log(`Error: ${error.message}`);
}
}
run();
The axios-retry function installs two interceptors into the Axios instance:
The request interceptor initializes the retry state and configures response validation.
The response interceptor handles error processing and retry logic execution.
When axios-retry performs a retry, the request passes through the entire Axios interceptor chain again. So, understanding the execution order is especially important when we combine it with other interceptors. In the example above, the output will look something like this (when all retries fail):
Request interceptor registered after
Request interceptor registered before
Response error interceptor registered before
Retry attempt #1
Error: Request failed with status code 500
Request interceptor registered after
Request interceptor registered before
Response error interceptor registered before
Retry attempt #2
Error: Request failed with status code 500
Request interceptor registered after
Request interceptor registered before
Response error interceptor registered before
Retry attempt #3
Error: Request failed with status code 500
Request interceptor registered after
Request interceptor registered before
Response error interceptor registered before
Response error interceptor registered after
Response error interceptor registered after
Response error interceptor registered after
Response error interceptor registered after
Error: Request failed with status code 500
Each retry follows this sequence:
Request interceptor registered after
Request interceptor registered before
Response error interceptor registered before
Retry attempt #N
Error: Request failed with status code 500
As we mentioned, request interceptors are executed in reverse registration order. Then, the response interceptors are executed in order. Notice that the last interceptor registered never appears during retry attempts; it only shows up after all retries fail. When a request fails, axios-retry's response interceptor catches the error and decides whether to retry.
If retryable, it creates a new request, so the error never reaches subsequent interceptors.
If not retryable, the error is rejected and continues through the interceptor chain.
After 3 failed retries, the error flows through all remaining response error interceptors:
Response error interceptor registered after (x4)
Error: Request failed with status code 500
The "after" interceptor runs 4 times because:
1 time for the original request failure.
3 times for each retry failure (when errors are finally rejected).
We can configure the following parameters:
retries: Number of retry attempts before failing. The default value is3.retryCondition: Callback to determine if the request should be retried.shouldResetTimeout: Whether timeout resets between retries. The default value isfalse.retryDelay: Delay function between retries (in ms).onRetry: Callback executed before each retry attempt.onMaxRetryTimesExceeded: Callback when all retries are exhausted.validateResponse: Callback to determine if response should be resolved/rejected.
Additionally, axios-retry provides several built-in functions to help with parameter configuration, organized into the following categories:
Error Classification Functions (for retryCondition)
isNetworkError: Detects network connectivity issues.isRetryableError: Checks for HTTP errors worth retrying (429, 5xx).isSafeRequestError: Combines theisRetryableErrorcheck with safe HTTP methods (GET,HEAD,OPTIONS).isIdempotentRequestError: Combines theisRetryableErrorcheck with idempotent methods (GET,HEAD,OPTIONS,PUT,DELETE).isNetworkOrIdempotentRequestError:isNetworkErrororisIdempotentRequestError. Default value for theretryConditionparameter.
Delay Functions (for retryDelay)
noDelay: No delay between retries. Default value for theretryDelayparameter.exponentialDelay: Exponential backoff with 20% randomization.linearDelay: Linear delay progression, accepts custom delay factor.
All delay functions automatically respect the Retry-After header when present.
Testing: axios-mock-adapter
When writing tests, we should not hit real APIs. The axios-mock-adapter package intercepts requests at the adapter level to return mock data. Install it by running the command npm install axios-mock-adapter --save-dev.
import axios from 'axios';
import AxiosMockAdapter from "axios-mock-adapter";
const client = axios.create({
baseURL: 'http://localhost:3000'
});
const mock = new AxiosMockAdapter(client);
mock.onGet("/api/success").reply(200, {
users: [{ id: 1, name: "John Smith" }],
});
async function run() {
const response = await client.get('/api/success');
console.log(response.data);
}
run();
The axios-mock-adapter package intercepts requests by replacing the Axios adapter, not by adding interceptors. This approach allows it to mock requests before they reach the network while preserving the normal interceptor flow. During the AxiosMockAdapter creation, we can configure:
delayResponse: delay for all responses in milliseconds.onNoMatch: Behavior when no handler matches. Values arepassthroughorthrowException
For each HTTP method handler, we can configure the URL matching:
String: Exact URL match.RegExp: Pattern matching.Undefined: Match any URL.
Besides that, we can also set up matching by parameters, headers, and even the request body data. Each handler supports these response methods:
reply: Returns a static(status, data, and headers) or dynamic(a function that returns a tuple, status, data, and headers) response.replyOnce: One-time responsewithDelayInMs: Delay per request.passThrough: Forward the request to the real server.networkError: Simulate network error.timeout: Simulate timeout.abortRequest: Simulate aborted request.
Combining Multiple Interceptors
Real-world applications often need multiple interceptor functionalities working together. Understanding how to combine them effectively is crucial for building robust HTTP clients. The following example combines three interceptors to create a production-ready API client with logging, automatic retries, and token-based authentication.
import axios from 'axios';
import * as AxiosLogger from 'axios-logger';
import axiosRetry from 'axios-retry';
import tokenProvider from 'axios-token-interceptor';
const client = axios.create({
baseURL: 'http://localhost:3000',
});
client.interceptors.response.use(
response => {
const duration = Date.now() - response.config.metadata.startTime;
console.log(`Request duration: ${duration}ms`);
return AxiosLogger.responseLogger(response);
},
error => {
const retryState = error.config['axios-retry'];
const isLastAttempt = retryState?.retryCount === retryState?.retries;
if (isLastAttempt) {
if (error.config?.metadata?.startTime) {
const duration = Date.now() - error.config.metadata.startTime;
console.log(`Request duration (error): ${duration}ms`);
}
return AxiosLogger.errorLogger(error);
}
return Promise.reject(error);
}
);
axiosRetry(client, {
retries: 3,
retryDelay: axiosRetry.exponentialDelay,
retryCondition: (error) => {
return axiosRetry.isNetworkOrIdempotentRequestError(error);
},
onRetry: (retryCount, error, requestConfig) => {
console.log(`Retry attempt #${retryCount}`);
}
});
client.interceptors.request.use(
tokenProvider({
getToken: () => {
return "ABC";
}
})
);
client.interceptors.request.use(
config => {
const retryState = config['axios-retry'];
if (!retryState) {
config.metadata = { startTime: Date.now() };
return AxiosLogger.requestLogger(config);
}
return config;
}
);
async function run() {
try {
const response = await client.get('/api/random-fail?percentage=50');
console.log(response.data);
} catch (error) {
console.log(`Error: ${error.message}`);
}
}
run();
The client instance has the following pipeline for every request:
Starts a timer and calls
AxiosLogger.requestLogger(only once, not on retries).axios-token-interceptorinjects the token.axios-retryinitializes the retry state.Request is executed.
axios-retryhandles retry logic execution.On error, logs duration and error using
AxiosLogger.responseLoggeronly for the final retry attempt.On success, logs duration and response using
AxiosLogger.responseLogger.
Axios interceptors provide a powerful mechanism for implementing cross-cutting concerns in your HTTP layer. By understanding and properly utilizing interceptors, we can create robust, maintainable, and feature-rich API clients. Thanks, and happy coding. You can find all the code here.




