DEV Community

Cover image for Axios: Setup, Configuration, and Structuring
Cristafovici Dan
Cristafovici Dan

Posted on • Originally published at Medium

Axios: Setup, Configuration, and Structuring

Data fetching remains one of the most important parts of each application. The times when a simple landing page with static information could generate leads are gone. Nowadays, modern applications are purely focused on exchanging data between the client and server.

In this article, I’ve described my personal approach to creating a scalable and easy-to-maintain solution for data fetching. By incorporating data caching mechanisms, interceptors, and a state manager into your project, you can enhance its efficiency.

Just a heads up, the article won’t go through every single line of code. It’s more about showing you how to use Axios and React Query together in a better way. I assume you’re already familiar with both tools, so let’s dive into structuring and use them together.

Axios base setup

To configure the base setup of Axios, we call the axios module and apply the create method. Currently, we only provide two options: baseURL (the main path to the server) and default headers for the client.

export const client = (() => {
  return axios.create({
    baseURL: process.env.REACT_BASE_URL,
    headers: {
      Accept: "application/json, text/plain, */*",
    },
  });
})();
Enter fullscreen mode Exit fullscreen mode

Please note that the function “client” is an immediately invoked function expression, which means it’s called right after being defined. It’s crucial to create an instance of the axios query only once, when the axios module is loaded. This follows the singleton pattern commonly used in JavaScript.

const request = async (options: AxiosRequestConfig) => {
  const onSuccess = (response: AxiosResponse) => {
    const { data } = response;
    return data;
  };

  const onError = function (error: AxiosError) {
    return Promise.reject({
      message: error.message,
      code: error.code,
      response: error.response,
    });
  };

  return client(options).then(onSuccess).catch(onError);
};

export default request;
Enter fullscreen mode Exit fullscreen mode

In the request, we have two different functions: onSuccess and onError. Each of them returns an Axios instance of Response/Error. Using the promise structure, we call then when the request is successful and the catch when the request fails. This approach offers two powerful advantages:

  1. We can restructure data in case of a successful response, helping us avoid code duplication.
  2. We can return custom error messages in case of a failed request, enabling us to adjust the error messages according to the specifications of our project.

Axios Interceptors

There are two interceptors: one for requests and one for responses.

Request Interceptor:

client.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    const accessToken = localStorage.getItem(STORAGE_TOKEN.ACCESS_TOKEN);
    if (accessToken) {
      config.headers.Authorization = `Bearer ${accessToken}`;
    }
    return config;
  },
  (error: AxiosError) => {
    return Promise.reject(error);
  },
);
Enter fullscreen mode Exit fullscreen mode

Before the client sends the request to the server, it can be modified. In the following example, we retrieve the accessToken from LocalStorage and set it in the global config, which we receive as a parameter for our function. After adding the Bearer token, we return the modified config. This step is necessary because if it is a restricted UI (dashboard, admin panels, etc.), all requests require authorization. Additionally, note that in case of an error, we can return Promise.reject with the occurred error.

Response interceptor:

client.interceptors.response.use(
  (res: AxiosResponse) => {
    return res; // Simply return the response
  },
  async (err) => {
    const status = error.response ? error.response.status : null;

    if (status === 401) {
      try {
        const refreshTokenFromStorage = localStorage.getItem(
          STORAGE_TOKEN.REFRESH_TOKEN
        );
        const { accessToken, refreshToken } = await AuthService.refresh(
          refreshTokenFromStorage
        );

        LocalStorageService.setTokens(accessToken, refreshToken);
        client.defaults.headers.common.Authorization = `Bearer ${accessToken}`;

        return await client(originalConfig);
      } catch (error: AxiosError) {
        return Promise.reject(error);
      }
    }

    if (status === 403 && err.response.data) {
      return Promise.reject(err.response.data);
    }

    return Promise.reject(err);
  }
);
Enter fullscreen mode Exit fullscreen mode

In the second interceptor, we already modify the data received from the server. If the server responds correctly, we simply return the response. However, if there’s an error, we take specific actions. Firstly, we need to identify the error sent by the server, typically done by checking standardized statuses. In the following code, we check the status to determine the appropriate action. If it’s a 401 error, we refresh tokens using refreshToken, which has a longer expiry period. For a 403 error, we throw a Forbidden Error. This is an example of how we can globally handle every request. Keep in mind that based on your project specifications, you can handle errors in any desired way. If your server sends specific errors, you can handle them based on other parameters.

Project structure

So far, we’ve learned how to set up the Axios configuration to make it easily changeable, customizable, and scalable. The final part of the Axios configuration would be an example of how to structure and use the configured request.

The primary purpose of structuring Axios in this manner is to organise requests based on their business domain, and they must access the same backend-controller.

Configuration file

This is the basic structure of our config.ts file. It keeps the endpoints as constants in the form of key-value pairs. The value is a function, allowing us to easily manage and call them, providing the necessary parameters when needed.

export const ProductEndpoints = {
  getMyProducts: () => "product/me",
  getProduct: (id: string | number) => `product/${id}`,
  getProductsFromOrder: (orderToken: string) => `/product/order-products?token=${orderToken}`,
};
Enter fullscreen mode Exit fullscreen mode

Service file

Let’s look at the service file. We group API calls by business domain into a class, all related to products in our case. Each method calls a specific endpoint using a function from the config file as the URL. When sending query parameters, we provide them as parameters to avoid sending incorrect strings with empty parameters.

export default class ProductService {
  public static readonly getMyProducts = ({
    offset,
    limit,
    textFilter,
  }: ProductQueryParams): Promise<Product[]> => {
    return request({
      url: ProductEndpoints.getMyProducts(),
      method: AxiosMethod.GET,
      params: {
        limit,
        offset,
      },
    });
  };

  public static readonly getProduct = (
    id: string | number,
  ): Promise<Product> => {
    return request({
      url: ProductEndpoints.getProduct(id),
      method: AxiosMethod.GET,
    });
  };

  public static readonly getProductsFromOrder = (
    token: string,
  ): Promise<Product[]> => {
    return request({
      url: ProductEndpoints.getProductsFromOrder(token),
      method: AxiosMethod.GET,
    });
  };
}
Enter fullscreen mode Exit fullscreen mode

This concludes Part 1. We performed the initial Axios configuration, set up interceptors, and established the folder structure for API calls. In Part 2 ,we’ll implement queries and mutations (using React Query) in our project and set up some global configurations.

Top comments (0)