Axios: Setup, Configuration, and Structuring
Tags: javascript, react, api
Published: 2024-05-06 06:38:22

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.

```typescript
export const client = (() => {
  return axios.create({
    baseURL: process.env.REACT_BASE_URL,
    headers: {
      Accept: "application/json, text/plain, */*",
    },
  });
})();
```
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.

```typescript
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;
```

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.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l2okrwmw817ys6j5hkdp.png)


**Request Interceptor:**
```typescript
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);
  },
);
```

> 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:**

```typescript
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);
  }
);
```

> 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.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7c8c6vh3majllt3avp6f.png)

**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.

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

**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.

```typescript
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,
    });
  };
}
```


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.