DEV Community

Junior
Junior

Posted on

5 tips to improve backend integration, React with Axios

Introduction

In most frontend applications we have to integrate with the backend and with that comes several libraries that we can use such as fetch, ajax, axios among others and each one has its characteristics, advantages and disadvantages.

Bibliotecas de integrações com backend

But regardless of which one we are going to use in our application, we have to think of points to help us with maintenance and better communication so as not to affect the usability of the user.

In this post I will use axios with react and applying concepts that I consider extremely important that we should address in our applications. I will take into account that you already have a react project with axios installed.

Logo axios

1 - Encapsulate service

We must create a generic service called with the library that we chose to use for the integration and simply just use it in the application, with the same idea of components like card, inputs, among others that we already do.

Serviço

First we have to create a http.js or http.ts file (Remembering that you can put another name if you want) to export the axios with the base url of our backend already configured.

import Axios from 'axios';

const http = Axios.create({
  baseURL: process.env.REACT_APP_URL,
});

export default http;
Enter fullscreen mode Exit fullscreen mode

Now we must create another index.js or index.ts file where we will export the most commonly used http methods already wrapped in a try catch blog to deal with the errors of the calls right here. Here we are already using the file we created above http.ts to trigger the axios with the parameters, in future posts we will evolve this file.

import http from './http';

export default {
  async get(url: string) {
    try {
      const response = await http.get(url);
      return response;
    } catch (err: any) {
      return false;
    }
  },
  async post(url: string, send: object) {
    try {
      const response = await http.post(url, send);
      return response;
    } catch (err: any) {
      return false;
    }
  },
  async put(url: string, send: object) {
    try {
      const response = await http.put(url, send);
      return response;
    } catch (err: any) {
      return false;
    }
  },
  async delete(url: string) {
    try {
      await http.delete(url);
      return true;
    } catch (err: any) {
      return false;
    }
  },
};
Enter fullscreen mode Exit fullscreen mode

In the end we will have a folder structure like this.

Estrutura de pasta

We'll be able to invoke the method on our components this way.

await services.post( '/authenticate', { email, password } );

But why is it necessary to use this approach?

Porque devemos usar

When we work with a generic service and only import it into our application, it becomes simpler to maintain and modify later. See how we can do it below.

2 - Add headers to all requests

Now let's configure headers for all our requests, we'll need this point to pass token among other information that your backend may need as a business rule.

Let's create an axios interceptors for this, as it's the best way to not just repeat code. See the example below.

import Axios, { AxiosRequestConfig } from 'axios';

const http = Axios.create({
  baseURL: process.env.REACT_APP_URL,
});

http.interceptors.request.use((config: AxiosRequestConfig) => {
  const token = window.localStorage.getItem('token');
  if (!token) return config;
  if (config?.headers) {
    config.headers = { Authorization: `Bearer ${token}` };
  }
  return config;
});

export default http;
Enter fullscreen mode Exit fullscreen mode

Here we have already retrieved the localstorage token and added it to all calls to the backend.

3 - Redirect unauthorized or unauthenticated user

We must have user redirection strategies when the user does not have authorization or permission so that he does not have the need to do this in our components.

For that we must create another interceptors to deal with this process.

import Axios, { AxiosRequestConfig } from 'axios';

const http = Axios.create({
  baseURL: process.env.REACT_APP_URL,
});

http.interceptors.request.use((config: AxiosRequestConfig) => {
  const token = window.localStorage.getItem('token');
  if (!token) return config;
  if (config?.headers) {
    config.headers = { Authorization: `Bearer ${token}` };
  }
  return config;
});

http.interceptors.response.use(
  (value) => {
    return Promise.resolve(value);
  },
  (error) => {
    const { isAxiosError = false, response = null } = error;

    if (isAxiosError && response && response.status === 401) {
      // User redirection rule for login page
      return Promise.reject(error);
    }
    if (isAxiosError && response && response.status === 403) {
      // User redirection rule for disallowed page
      return Promise.reject(error);
    }
    return Promise.reject(error);
  }
);

export default http;

Enter fullscreen mode Exit fullscreen mode

Leave open where you want to send the user for both 401(Unauthenticated) and 403(Unauthorized). That way, even if the user manages to access a page that he couldn't, when the backend request comes back with the status code, the system will already direct him, this approach also works for when the token expires, which we'll see how to deal with later.

4 - Request retry pattern

Now we will need to apply a pattern retry to our requests so that our end user does not suffer from instabilities in the application as it may be undergoing a deploy or auto scaling of the infrastructure at the time of the call. For this we define a number of attempts in case the system returns error 500 or higher. Example below.

import Axios, { AxiosRequestConfig } from 'axios';

const http = Axios.create({
  baseURL: process.env.REACT_APP_URL,
});

http.interceptors.request.use((config: AxiosRequestConfig) => {
  const token = window.localStorage.getItem('token');
  if (!token) return config;
  if (config?.headers) {
    config.headers = { Authorization: `Bearer ${token}` };
  }
  return config;
});

http.interceptors.response.use(
  (value) => {
    return Promise.resolve(value);
  },
  (error) => {
    const { isAxiosError = false, response = null } = error;

    if (isAxiosError && response && response.status === 401) {
      // Regra de redirecionamento de usuário para página de login
      return Promise.reject(error);
    }
    if (isAxiosError && response && response.status === 403) {
      // Regra de redirecionamento de usuário para página de não permitido
      return Promise.reject(error);
    }
    return Promise.reject(error);
  }
);

let counter = 1;

http.interceptors.response.use(
  (response) => {
    return response;
  },
  (error) => {
    if (
      error.response.status >= 500 &&
      counter < Number(process.env.REACT_APP_RETRY)
    ) {
      counter++;
      return http.request(error.config);
    }
    counter = 1;
    return Promise.reject(error);
  }
);

export default http;
Enter fullscreen mode Exit fullscreen mode

An interceptor was created so that it has a retry according to the number defined in process.env.REACT_APP_RETRY times when the request has a status code greater than 500.

5 - Refresh token

When we work with authentication, it is a good practice and security rule to have tokens that expire so that the user does not stay logged in forever even without using the application.

However, we have to solve the problem that if the token expires when the user cannot simply ask him to log in again, for that we have routes to refresh token.

We can improve our index.ts file so that it does this automatically during your application's route calls.

import http from './http';

async function refreshToken() {
  const value = Number(localStorage.getItem('expired'));
  if (value && new Date(value) < new Date()) {
    const result = await http.get('/refresh');
    localStorage.setItem('token', result.data.token);
    localStorage.setItem(
      'expired',
      String(new Date().setSeconds(result.data.expired))
    );
  }
}

export default {
  async get(url: string) {
    try {
      await refreshToken();
      const response = await http.get(url);
      return response;
    } catch (err: any) {
      return false;
    }
  },
  async post(url: string, send: object) {
    try {
      await refreshToken();
      const response = await http.post(url, send);
      return response;
    } catch (err: any) {
      return false;
    }
  },
  async put(url: string, send: object) {
    try {
      await refreshToken();
      const response = await http.put(url, send);
      return response;
    } catch (err: any) {
      return false;
    }
  },
  async delete(url: string) {
    try {
      await refreshToken();
      await http.delete(url);
      return true;
    } catch (err: any) {
      return false;
    }
  },
};
Enter fullscreen mode Exit fullscreen mode

We created a refreshToken() function that will always be called before all our application calls. It would check if the token's expired has already passed, and if so, make a new call to the backend, renewing the token and the expired. Remembering that this logic works according to the backend and the refresh route, for example, has a time limit after passing from expired to renewing the token, that would be more of a business rule.

Conclusion

In this post we saw five ways to improve our communication with the backend and taking into account the best experience for the end user, there are many other approaches that can improve our backend call service, but just by implementing these concepts we will have better maintenance and usability of our system. In future posts we will see how to further improve this service.

Obrigado até a próxima

References

Axios - https://axios-http.com/docs/intro
React - https://reactjs.org/

Latest comments (1)

Collapse
 
naucode profile image
Al - Naucode

Hey, that was a nice read, you got my follow, keep writing 😉