React, TypeScript, Axios, and Interceptors: Improving HTTP Request Management

Over the past few months, I have been working on a personal project, Courier App, where I use microservices on the server side and React with TypeScript on the client side. Authentication generates tokens (accessToken and refreshToken) that are stored in cookies. These cookies are automatically sent with every request to protected endpoints, enhancing security and simplifying the request process.

Server Configuration

For example, in one of my microservices, I have the following security configuration:

public class SecurityConfig {
    private final AuthFilter authFilter;
    private final ExceptionHandlerFilter exceptionHandlerFilter;

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf(AbstractHttpConfigurer::disable)
            .addFilterBefore(exceptionHandlerFilter, UsernamePasswordAuthenticationFilter.class)
            .addFilterAfter(authFilter, ExceptionHandlerFilter.class)
            .authorizeHttpRequests(request -> request 
                .requestMatchers("/api/courier/**").authenticated())
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
        return http.build();
    }
}        

Client Configuration

Instead of manually adding the user token to every request to endpoints like “/api/courier/**”, I understood that a custom Axios instance could automatically add the user token if it exists. This can be achieved using interceptors.

Storing Tokens in Cookies

A significant improvement in my current implementation is that both the accessToken and refreshToken are now stored in cookies. This change enhances security by leveraging the browser's cookie storage with options like HttpOnly and Secure, which help protect the tokens from cross-site scripting (XSS) attacks.

Key Parts of the Implementation

  • Setting up the Axios Base Configuration

First, I set up the Axios instance with a base URL and ensured that the requests are sent with credentials (cookies) using the withCredentials option:

const api = axios.create({
        baseURL: 'http://localhost:8080/api',
        headers: {
            'Content-Type': 'application/json',
        },
        withCredentials: true
    });        

  • Handling Successful Response

The response interceptor checks if the HTTP status code indicates success. If so, the response is resolved; otherwise is rejected:

const status = (response: AxiosResponse): Promise<AxiosResponse> => {
    if (response.status >= 200 && response.status < 300) {
        return Promise.resolve(response);
    }
    return Promise.reject(response);
};

const onResponse = (response: AxiosResponse): Promise<AxiosResponse> => {
    return status(response);
};        

  • Handling Token Expiration

If a request fails with a 401 status (Unauthorized), the response interceptor will trigger the OnResponseError function. This function checks if the request has already been retried. If not, it attempts to refresh the token using the "/auth/refresh" endpoint and retries the original request with the new token:

const onResponseError = async (error: any): Promise<any> => {
    const originalRequest = error.config;

    if (error.response && error.response.status === 401 && !originalRequest._retry) {
        originalRequest._retry = true;

        try {
            await api.post('/auth/refresh', {}, { withCredentials: true });
            return api(originalRequest);
        } catch (refreshError) {
            console.error('Error during token refresh:', refreshError);
            return Promise.reject(refreshError);
        }
    }

    return Promise.reject(error);
};        

Conclusion

By utilizing Axios interceptors and securely storing tokens in cookies, I've significantly improved how my application handles HTTP requests, particulary in managing token-based authentication. This approach provides a robust solution to ensure secure and seamless communication between the client and server.

I hope this experience is useful to you, and I encourage you to explore using Axios interceptors and cookie-based token storage in your projects.

Complete Code

Here is the complete code for setting up Axios interceptors in my project:

import axios, { AxiosResponse, InternalAxiosRequestConfig } from "axios";

const status = (response: AxiosResponse): Promise<AxiosResponse> => {
    if(response.status >= 200 && response.status < 300){
        return Promise.resolve(response);
    }

    return Promise.reject(response);
}

export const service = (() => {

    const api = axios.create({
        baseURL: 'http://localhost:8080/api',
        headers: {
            'Content-Type': 'application/json',
        },
        withCredentials: true
    });

    const onRequest = (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => {  
        return config;
    }

    const onRequestError = (error: any): Promise<any> => {
        return Promise.reject(error);
    }

    const onResponse = (response: AxiosResponse): Promise<AxiosResponse> => {
        return status(response);
    }

    const onResponseError = async(error: any): Promise<any> => {
        const originalRequest = error.config;

        if(error.response && error.response.status === 401 && !originalRequest._retry){
            originalRequest._retry = true;

            try{
                await api.post('/auth/refresh', {}, { withCredentials: true });

                return api(originalRequest);
            }catch(refreshError){
                console.error('Error during token refresh:', refreshError);
                return Promise.reject(refreshError);
            }
        }
    }

    api.interceptors.request.use(onRequest, onRequestError);
    api.interceptors.response.use(onResponse, onResponseError);

    return api;
})();        


To view or add a comment, sign in

Others also viewed

Explore content categories