Hello 👋! In this blog, I will show you to create an axios client using interceptors to use with an app that requires JWT authentication. In this case, we will use React, but in can easily be ported to another framework.
We will use the backend from this blog post.
For this, I've created a starter repository for us to focus only on the refresh token part. You can clone it with this command:
npx degit mihaiandrei97/blog-refresh-token-interceptor react-auth
Inside it, you will see two folders:
- react-auth-start: here is the code that you will be using for this project.
- react-auth-finished: here is the final code, if you missed something and you need to check it.
Or if you want to see the final code, you can check it here.
Project explanation
The application has 2 pages:
- A Login page, with a form where the user can register/login and after that we save the tokens in localStorage.
- A Home page where we display the user profile if he is logged in.
I like to keep all of my api calls inside a file/folder called services
. With this approach, I can see all the calls used in the app.
Step 1 - Create Axios Interceptor for request
As a first step, let's define the axios interceptors. You can read more about them here, but as a simple explanation, we will use them to execute some code before we make a request, or after we receive a response.
This is what we will implement:
Let's create a file inside lib
called app/lib/axiosClient.js
:
Here, we will define a function that will create our axios instance. We will use that instance everywhere in the app, instead of axios
. If we do that, for each request/response, our interceptors will be executed.
import axios from "axios";
class AxiosInterceptor {
constructor(instanceConfig = {}) {
// Initialize Axios instance with provided configuration
this.axiosInstance = axios.create({
...instanceConfig,
});
// Add request interceptor
this.axiosInstance.interceptors.request.use(
(config) => {
const accessToken = this.getAccessToken();
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
},
(error) => Promise.reject(error),
);
// Bind instance methods for convenience
this.get = this.axiosInstance.get.bind(this.axiosInstance);
this.post = this.axiosInstance.post.bind(this.axiosInstance);
this.put = this.axiosInstance.put.bind(this.axiosInstance);
this.delete = this.axiosInstance.delete.bind(this.axiosInstance);
}
getAccessToken() {
return localStorage.getItem("accessToken");
}
}
// Export a pre-configured instance of AxiosInterceptor
export const client = new AxiosInterceptor({
baseURL: "http://localhost:5000/api/v1",
});
In this file, we define the class AxiosInterceptor
and we create an instance of axios with the provided configuration. We also add a request interceptor that will add the Authorization
header with the accessToken
from localStorage.
Step 2 - Create the services
Now, let's create the app/lib/services.js
file, where we would store all of our api calls.
import { client } from "./axiosClient";
export function register({ email, password }) {
return client.post("auth/register", { email, password });
}
export function login({ email, password }) {
return client.post("auth/login", { email, password });
}
export function getProfile() {
return client.get("/users/profile");
}
Here, we imported the client instance, and we use it to make requests like we would normally do with the axios
keyword.
And now we can use these functions in our components. Let's go to the Login
component and add the login/register functionality. Let's add the clientAction
that is going to be called when the form is submitted.
export async function clientAction({ request }) {
try {
const formData = await request.formData();
const type = formData.get("type");
const email = formData.get("email");
const password = formData.get("password");
const response =
type === "register"
? await register({ email, password })
: await login({ email, password });
const { accessToken, refreshToken } = response.data;
localStorage.setItem("accessToken", accessToken);
localStorage.setItem("refreshToken", refreshToken);
return redirect("/");
} catch (error) {
return {
error: error?.response?.data?.message || error.message,
};
}
}
In this function, we get the form data and we call the register
or login
function from the services.js
file. After that, we save the tokens in localStorage and redirect the user to the home page.
Next, let's go to the Home
page.
There, let's add the getProfile
function that will be called when the component mounts.
export async function clientLoader() {
try {
const accessToken = localStorage.getItem("accessToken");
if (!accessToken) {
return redirect("/login");
}
const { data } = await getProfile();
return { profile: data };
} catch {
localStorage.removeItem("accessToken");
return redirect("/login");
}
}
In this function, we check if the user is logged in. If not, we redirect him to the login page. If he is logged in, we call the getProfile
function and we return the user profile.
We can also add a Logout
button and display the profile.
export async function clientAction() {
localStorage.removeItem("accessToken");
localStorage.removeItem("refreshToken");
return null;
}
export default function Home(props) {
return (
<div className="min-h-screen bg-gray-100 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-3xl mx-auto bg-white shadow-md rounded-lg overflow-hidden">
<div className="px-6 py-8">
<h1 className="text-3xl font-bold text-center text-gray-900 mb-8">
Welcome to React Auth!
</h1>
<div className="bg-gray-50 rounded-lg p-4 mb-8 overflow-auto">
<pre className="text-sm text-gray-700">
<code>{JSON.stringify(props.loaderData, null, 2)}</code>
</pre>
</div>
<div className="flex flex-col sm:flex-row justify-center items-center space-y-4 sm:space-y-0 sm:space-x-4">
<Form method="post">
<button
type="submit"
className="w-full sm:w-auto bg-red-600 hover:bg-red-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline transition duration-150 ease-in-out"
>
Logout
</button>
</Form>
</div>
</div>
</div>
</div>
);
}
Step 3 - Create Axios Interceptor for response
Here, things get a bit more complicated, but don't worry, I will explain everything.
The final code for axios client will look like this:
import axios from "axios";
/**
* A class to intercept Axios requests and handle token-based authentication.
* It retries unauthorized (401) requests after refreshing the access token.
*/
class AxiosInterceptor {
/**
* Creates an AxiosInterceptor instance.
* @param {object} [instanceConfig={}] - Configuration for the Axios instance.
*/
constructor(instanceConfig = {}) {
this.isRefreshing = false; // Tracks if a token refresh is in progress
this.refreshSubscribers = []; // Queue for requests waiting for the token to refresh
// Initialize Axios instance with provided configuration
this.axiosInstance = axios.create({
...instanceConfig,
});
// Add request interceptor
this.axiosInstance.interceptors.request.use(
(config) => {
const accessToken = this.getAccessToken();
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
},
(error) => Promise.reject(error),
);
// Add response interceptor
this.axiosInstance.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (
error.response &&
error.response.status === 401 &&
error.response.data.message === "TokenExpiredError" &&
!originalRequest._retry
) {
if (!this.isRefreshing) {
this.isRefreshing = true;
try {
const newTokens = await this.refreshTokens();
this.setAccessToken(newTokens.accessToken);
this.setRefreshToken(newTokens.refreshToken);
this.refreshSubscribers.forEach((callback) =>
callback(newTokens.accessToken),
);
this.refreshSubscribers = [];
return this.axiosInstance(originalRequest);
} catch (refreshError) {
this.refreshSubscribers = []; // Clear the queue in case of failure
this.setAccessToken("");
this.setRefreshToken("");
return Promise.reject(refreshError);
} finally {
this.isRefreshing = false;
}
}
return new Promise((resolve) => {
this.refreshSubscribers.push((newAccessToken) => {
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
originalRequest._retry = true;
resolve(this.axiosInstance(originalRequest));
});
});
}
return Promise.reject(error);
},
);
// Bind instance methods for convenience
/**
* Makes a GET request.
* @type {import('axios').AxiosInstance['get']}
*/
this.get = this.axiosInstance.get.bind(this.axiosInstance);
/**
* Makes a POST request.
* @type {import('axios').AxiosInstance['post']}
*/
this.post = this.axiosInstance.post.bind(this.axiosInstance);
/**
* Makes a PUT request.
* @type {import('axios').AxiosInstance['put']}
*/
this.put = this.axiosInstance.put.bind(this.axiosInstance);
/**
* Makes a DELETE request.
* @type {import('axios').AxiosInstance['delete']}
*/
this.delete = this.axiosInstance.delete.bind(this.axiosInstance);
}
/**
* Retrieves the current access token from localStorage.
* @returns {string|null} The access token or null if not available.
*/
getAccessToken() {
return localStorage.getItem("accessToken");
}
/**
* Stores a new access token in localStorage.
* @param {string} token - The new access token.
*/
setAccessToken(token) {
localStorage.setItem("accessToken", token);
}
/**
* Retrieves the current refresh token from localStorage.
* @returns {string|null} The refresh token or null if not available.
*/
getRefreshToken() {
return localStorage.getItem("refreshToken");
}
/**
* Stores a new refresh token in localStorage.
* @param {string} token - The new refresh token.
*/
setRefreshToken(token) {
localStorage.setItem("refreshToken", token);
}
/**
* Refreshes the authentication tokens by calling the refresh endpoint.
* @returns {Promise<{ accessToken: string, refreshToken: string }>} The new access and refresh tokens.
* @throws {Error} If the token refresh request fails.
*/
async refreshTokens() {
const refreshToken = this.getRefreshToken();
if (!refreshToken) {
throw new Error("No refresh token available");
}
const response = await this.axiosInstance.post("/auth/refreshToken", {
refreshToken,
});
return response.data; // Expecting { accessToken: string, refreshToken: string }
}
}
// Export a pre-configured instance of AxiosInterceptor
export const client = new AxiosInterceptor({
baseURL: "http://localhost:5000/api/v1",
});
The workflow can be seen in this diagram:
Now, let's take it step by step.
The queue implementation
We need to keep track of the requests that are waiting for the token to refresh. We will use an array called refreshSubscribers
for this, where we put the requests that failed at the same time when we tried to refresh the token. When is this usefull? When we have multiple calls in paralel. If we make 4 requests, we probably don't want each of them to trigger a refreshToken. In that case, only the first one triggers it, and the other ones are put in the queue, and retried after the refresh is finished.
Refresh token logic
- When we receive a 401 error, we check if the error is caused by the fact the token is expired;
- If it is, we check if we are already refreshing the token;
- If we are, we add the request to the queue and we return a promise that will be resolved when the token is refreshed;
- If not, we set the
isRefreshing
flag totrue
and we call therefreshTokens
function; - If the refresh is successful, we save the new tokens in localStorage, we call the requests from the queue and we clear the queue;
- If the refresh fails, we clear the queue and we remove the tokens from localStorage.
Conclusion
And that's it. Now, by using axios interceptors, your app should automatically add the access token to the header and also handle the refresh token silently, in order to keep the user authenticated 🎉🎉🎉.
Top comments (9)
Hi great article 👍.
Please can you share the link to a github repository (if any)?
The thing, I try to implement this using reduxjs and as you use zustand I wanted to know how you define the store. For instance when you do use AuthStore.getState().login I don't know if login is a reducer action or not so I'm a little bit confuse.
Hello! For sure. I thought it is understandable by default that degit goes to github for this:
npx degit mihaiandrei97/blog-refresh-token-interceptor react-auth
Here is the repo: github.com/mihaiandrei97/blog-refr...
I switched from React to Vue recently and I really don't want to go back so is there a simple way to use most of this code here in Vue without rewriting major portions of it? I'm fine with minor rewrites, that's expected but I don't want to basically rewrite the whole thing myself. Thanks for this and the backend article! They helped a ton!
The axios part is the same. The useEffects could be changed into created or watchers in vue
Okay, thanks!
Hello, I just want to ask question, I tried converting this to typescript and I don't have any luck.
This is error that I get:
And this happens when I call client.post()
Any help man.
I ran into the same issue. You need to overwrite the AxiosRequestConfig. Just place this anywhere in your code. I placed it on top of the createAxiosClient function.
Thanks! Good to know
Hello. Can you create a repo and share the url So I can check?