Streamlining JWT Handling in React with Axios Interceptors and a Custom Instance (TypeScript)
- Mark Kendall
- Feb 11
- 4 min read
## Streamlining JWT Handling in React with Axios Interceptors and a Custom Instance (TypeScript)
Managing JSON Web Tokens (JWTs) in a React frontend interacting with a Spring Boot backend can be tricky, especially when dealing with token expiration and refresh mechanisms. A common challenge is handling 403 (Forbidden) errors when access tokens expire during user sessions. This article demonstrates a robust approach using Axios interceptors within a custom Axios instance, all written in TypeScript, to seamlessly manage JWTs and refresh tokens in your React application.
### The Problem: Expired Tokens and 403 Errors
When your React application makes requests to protected endpoints on your Spring Boot backend, it includes the JWT in the `Authorization` header. If the access token has expired, the backend will return a 403 error. A naive approach would be to handle this error on a case-by-case basis within your components, leading to repetitive and error-prone code. A more elegant solution is to use Axios interceptors.
### The Solution: Axios Interceptors and a Custom Instance (TypeScript)
Axios provides interceptors, which are functions that can intercept requests and responses before they are handled by your application code. We'll leverage these, within a custom Axios instance, to create a centralized mechanism for managing JWTs and refreshing tokens. TypeScript will add type safety and improve code maintainability.
1. Creating a Custom Axios Instance (TypeScript):
```typescript
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
const api: AxiosInstance = axios.create({
baseURL: 'YOUR_API_BASE_URL', // Replace with your Spring Boot API URL
});
// ... (Interceptor code will go here)
export default api;
```
This creates a typed Axios instance (`api`) with your base URL. All requests made using this instance will automatically use this base URL.
2. Implementing the Refresh Token Logic (TypeScript):
```typescript
interface Tokens {
accessToken: string | null;
refreshToken: string | null;
}
const getTokens = (): Tokens => {
const accessToken = localStorage.getItem('accessToken');
const refreshToken = localStorage.getItem('refreshToken');
return { accessToken, refreshToken };
};
const refreshAccessToken = async (): Promise<string | null> => {
try {
const { refreshToken } = getTokens();
if (!refreshToken) {
console.error("No refresh token available. Redirecting to login.");
// Handle missing refresh token (e.g., redirect to login)
return null;
}
const refreshResponse: AxiosResponse<{ accessToken: string }> = await api.post('/refresh_token', { refreshToken }); // Your refresh endpoint
const newAccessToken = refreshResponse.data.accessToken;
localStorage.setItem('accessToken', newAccessToken); // Update local storage
return newAccessToken;
} catch (error) {
console.error("Error refreshing token:", error);
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
// Handle refresh token failure (e.g., redirect to login)
return null;
}
};
```
This code defines functions to retrieve tokens from local storage and refresh the access token by sending the refresh token to your backend's `/refresh_token` endpoint. The `Tokens` interface and return types add type safety.
3. Request Interceptor (TypeScript):
```typescript
api.interceptors.request.use(
async (config: AxiosRequestConfig): Promise<AxiosRequestConfig> => {
const { accessToken } = getTokens();
if (accessToken) {
config.headers = {
...config.headers,
Authorization: `Bearer ${accessToken}`,
};
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
```
This interceptor adds the access token to the `Authorization` header of every outgoing request. The TypeScript types ensure type correctness.
4. Response Interceptor (Handling 403 Errors - TypeScript):
```typescript
api.interceptors.response.use(
(response: AxiosResponse) => {
return response;
},
async (error: any) => { // Type 'any' here as error structure might vary
const originalRequest = error.config;
if (error.response?.status === 403 && !originalRequest._retry) {
originalRequest._retry = true;
const newAccessToken = await refreshAccessToken();
if (newAccessToken) {
originalRequest.headers = {
...originalRequest.headers,
Authorization: `Bearer ${newAccessToken}`,
};
return api(originalRequest); // Retry the original request
} else {
return Promise.reject(error); // Refresh failed
}
}
return Promise.reject(error);
}
);
```
This interceptor handles 403 errors and adds type safety. Note the use of optional chaining (`error.response?.status`) to handle cases where the error might not have a response object. The `error: any` is used because the exact structure of the error object can vary depending on the error type. You can create a more specific type if needed.
5. Using the `api` Instance in Your Components (TypeScript):
```typescript
import api from './api';
interface MyData {
// Define the type of data you expect from the API
// ...
}
const MyComponent: React.FC = () => {
const fetchData = async () => {
try {
const response = await api.get<MyData>('/my_protected_endpoint'); // Type the response
const data = response.data; // Access data with type safety
console.log(data);
} catch (error) {
console.error("Error fetching data:", error);
// Handle errors (e.g., redirect to login)
}
};
// ...
};
```
Import the `api` instance and use it to make your requests. The interceptors will automatically handle token management and refresh logic. The `<MyData>` generic types the API response, giving you type safety when accessing the data.
Key Takeaway:
This TypeScript implementation provides a clean and centralized way to manage JWTs in your React application. By leveraging Axios interceptors within a custom Axios instance, you can abstract away the complexities of token refresh and significantly reduce code duplication. Remember that you are using Axios's built-in interceptors functionality – not creating a separate interceptor class – when configuring the `api` instance. This is a best-practice approach for working with JWTs and Axios in a TypeScript project. The added TypeScript types significantly enhance code maintainability and prevent common errors.
Comments