Axios Interceptors as HTTP Middleware: A Mental Model for React Apps
Understand how Axios request and response interceptors work as middleware, what belongs in each, and how they centralize auth headers and 401 handling in React.
This article is published in English.
Sources: Axios Interceptors Explained: The Missing Piece Every Frontend Developer Should Learn.
In a young React codebase, API calls tend to grow one copy-paste at a time: each request builds its own Authorization header, each response gets its own error handling, and each failure path repeats the same recovery logic. That works for three endpoints and becomes a maintenance problem at thirty. Axios has a built-in answer, interceptors, and once you see them as middleware for HTTP traffic, it becomes clear what to move into them and what to leave in your components.
The duplication that interceptors remove
A typical starting point is a request for users that attaches a bearer token inline:
axios.get("/users", {
headers: {
Authorization: `Bearer ${token}`,
},
});
A second endpoint repeats the header block word for word:
axios.get("/products", {
headers: {
Authorization: `Bearer ${token}`,
},
});
And a write operation does the same, with the payload in the middle:
axios.post("/orders", orderData, {
headers: {
Authorization: `Bearer ${token}`,
},
});
None of this looks harmful at first. As the project grows, every request needs the token, every response needs error handling, and every failure needs the same reaction. That cross-cutting work is what interceptors are for.
What an interceptor actually is
An interceptor is a function Axios runs on every outgoing request before it is sent, or on every incoming response before your code sees it: a checkpoint where you can inspect or change the traffic. The reply gets its own checkpoint before it resolves the promise your React code is awaiting.
The overall path starts at your application:
React Application
and continues through both checkpoints before returning to it:
↓Request Interceptor↓Axios↓Server↓Response Interceptor↓React Application
Your components never talk to the server directly; everything flows through the chain, which makes it the natural home for uniform behavior.
Middleware, explained with a front desk
If you have written Express or Koa code, the concept is familiar: it is middleware. Otherwise, think about visiting an office building. You do not walk straight into the office. The path starts with you:
You
and passing through two stops on the way in:
↓Security Check↓Reception↓Office
Security checks your ID, reception confirms your appointment, and only then do you reach the office. Neither stop is the destination; each intercepts you to do extra work, then lets you continue or turns you away.
Middleware is any layer between two systems that does work before the request moves on. Swap the office for a server, starting from the React app:
React App
and the interceptor is the layer in the middle:
↓Middleware (Interceptor)↓Server
In other words, Axios interceptors are middleware for HTTP requests and responses on the client.
Request and response interceptors
Axios offers two kinds, registered with axios.interceptors.request.use(...) and axios.interceptors.response.use(...). Each call takes a success handler and an optional error handler, and returns an id you can later pass to eject to remove it.
Request interceptors
These run before a request is sent. The component issues the call:
Component
and the interceptor sees it on the way out:
↓Request Interceptor↓Server
Typical jobs for a request interceptor:
- attaching authentication tokens
- setting custom headers such as a locale or request id
- adjusting the request payload
- logging outgoing traffic
The handler receives the request config and must return it (or a promise of it) for the request to continue; forgetting this is the classic beginner mistake.
Response interceptors
These run after the server replies but before your application receives the result. The response comes from the server:
Server
and passes through the interceptor on its way to the component:
↓Response Interceptor↓Component
Common responsibilities here:
- handling errors in one place
- transforming or unwrapping response data
- logging responses
- retrying failed requests
- redirecting the user, for example to a login page
The success handler covers 2xx responses and the error handler the rest. If it cannot recover, it should return a rejected promise so callers still see the failure.
What happens behind a single call
A one-line request looks as if Axios fires it immediately:
axios.get("/users");
Internally, the same call:
axios.get("/users")
travels through a longer pipeline before your .then() runs:
↓Request Interceptor↓HTTP Request↓Server↓HTTP Response↓Response Interceptor↓.then()↓Your Component
Shared logic runs on every call without any call site knowing about it.
Why centralizing matters as the app grows
Picture a project with a hundred API calls written the manual way. The users request carries the header:
axios.get("/users", {
headers: {
Authorization: `Bearer ${token}`,
},
});
So does the products request:
axios.get("/products", {
headers: {
Authorization: `Bearer ${token}`,
},
});
And so does every write, such as creating an order:
axios.post("/orders", data, {
headers: {
Authorization: `Bearer ${token}`,
},
});
If the backend changes its authentication scheme, you must update every call, and each one you miss becomes a bug. An interceptor holds that logic once and Axios applies it everywhere. Register it on an instance created with axios.create(), so it applies to your API and not to third-party URLs.
Use case: attaching the access token automatically
This is the most common reason to adopt interceptors. After login the app stores an access token that every protected endpoint expects. The call starts as a plain request:
API Call
and the interceptor adds the header before it reaches the server:
↓Request Interceptor↓Authorization Header Added↓Server
Every request now carries the token. Read it inside the interceptor on each request rather than once at startup, or a refreshed token will never be picked up.
Use case: handling 401 responses globally
Access tokens expire. When that happens, the server answers with:
401 Unauthorized
Without interceptors, every call needs its own branch for this. The first API call:
API 1
is followed by the same handling duplicated for every other endpoint:
↓401 HandlingAPI 2↓401 HandlingAPI 3↓401 Handling
A response interceptor can instead catch every 401 Unauthorized in one place. When it sees one:
401 Received
it can refresh the access token, replay the original request, and hand the successful response back to the caller:
↓Refresh Access Token↓Retry Original Request↓Return Successful Response
The user never notices the expired token. Two safeguards matter: flag a replayed request on its config so a second 401 cannot loop forever, and make simultaneous failures share one refresh call, covered in why concurrent 401s log users out and the single-flight refresh fix.
When interceptors are not worth it
A small app with two or three API calls gains little from the indirection, and hidden behavior can surprise a reader who cannot see where a header came from. With dozens or hundreds of requests, interceptors are one of the best ways to keep request handling clean and consistent. Keep them for cross-cutting concerns; endpoint-specific logic belongs with the endpoint.
Wrapping up
Sending requests is a small part of an HTTP layer's job. Authentication, error handling, logging, response shaping and token refresh are cross-cutting, and interceptors give each a single home.
- Treat interceptors as client-side middleware: request interceptors shape what leaves, response interceptors shape what arrives.
- Always return the config from request interceptors and rethrow unrecoverable errors from response interceptors.
- Attach them to an
axios.create()instance scoped to your API. - Guard 401 retry logic against loops and concurrent refreshes.