Voltar para o blog

5 dicas para melhorar integração com o backend, React com Axios

Introdução

Na maior parte das aplicações frontend temos que fazer integrações com o backend e com isso vem várias bibliotecas que podemos usar como por exemplo fetch, ajax, axios entre outras e cada uma tem suas características, vantagens e desvantagens.

Bibliotecas de integrações com backend

Mas independente de qual vamos utilizar em nossa aplicação temos que pensar em pontos para nos ajudar na manutenção e uma melhor comunicação para não afetar a usabilidade do usuário.

Nesse post vou utilizar o axios com o react e aplicando conceitos que considero de extrema importância que devemos abordar em nossas aplicações. Vou levar em consideração que você já tem um projeto react com o axios instalado.

Logo axios

1 - Encapsular serviço

Devemos criar um serviço genérico de chamada com a biblioteca que escolhemos usar para fazer a integração e simplesmente só utilizar o mesmo na aplicação, com um tudo a mesma ideia de componentes como card, inputs, entre outros que já fazemos.

Serviço

Primeiro temos que criar um arquivo http.js ou http.ts (Lembrando você pode colocar outro nome caso queira) para exportar o axios com a base url do nosso backend já configurada.

import Axios from 'axios';

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

export default http;
Enter fullscreen mode Exit fullscreen mode

Agora devemos criar um outro arquivo index.js ou index.ts aonde vamos exportar os métodos comumente mais usados do http já envolvendo em um blog try catch para lidarmos com os erros das chamadas aqui mesmo. Aqui já estamos utilizando o arquivo que criamos acima do http.ts para acionar o axios com os parâmetros, em posts futuros vamos evoluir esse arquivo.

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

No final teremos uma estrutura de pasta assim.

Estrutura de pasta

Vamos poder invocar o método em nossos componentes dessa maneira.

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

Mas porque se faz necessário utilizar essa abordagem?

Porque devemos usar

Quando trabalhamos com serviço genérico e só importamos ele em nossa aplicação fica mais simples de realizar manutenção e de modificar posteriormente. Veja como podemos fazer abaixo.

2 - Adicionar headers a todas request

Agora vamos configurar headers a todas nossas request, vamos necessitar desse ponto para passar token entre outras informações que seu backend pode precisar como regra de negócio.

Vamos criar um interceptors do axios para isso, pois é a melhor maneira para não ficarmos só repetindo código. Veja o exemplo abaixo.

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

Aqui já recuperamos o token do localstorage e adicionamos a todas as chamadas ao backend.

3 - Redirecionamento usuário não autorizado ou não autenticado

Devemos ter estratégias de redirecionamento de usuário quando o mesmo não tiver autorização ou permissão para que não tenha a necessidade de fazer isso em nossos componentes.

Para isso devemos criar outro interceptors para lidar com esse processo.

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

export default http;

Enter fullscreen mode Exit fullscreen mode

Deixe aberto para onde você deseja mandar o usuário tanto para 401(Não autenticado) e 403 (Não autorizado). Dessa maneira mesmo que o usuário conseguir acessar uma página que não poderia quando a request do backend voltar com o status code o sistema já irá direcioná-lo, essa abordagem serve também para quando token expira, que vamos ver como lidar com isso mais adiante.

4 - Pattern retry de request

Agora vamos precisar aplicar ​​pattern retry em nossas requests para que o nosso usuário final não sofra com instabilidades na aplicação pois a mesma pode está passando por um deploy ou auto scaling da infraestrutura no momento da chamada. Para isso nós definimos um número de tentativas caso o sistema retorne erro 500 ou superior. Exemplo abaixo.

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

Foi criando um interceptors para que tenha retentativa de acordo com o número que for definido em process.env.REACT_APP_RETRY vezes quando a request tenha status code maior igual a 500.

5 - Refresh token

Quando trabalhamos com autenticação é uma boa prática e regra de segurança termos tokens que expiram para que o usuário não fique logado para sempre mesmo sem usar a aplicação.

Porém temos que resolver o problema que se o token expirado quando o usuário não podemos simplesmente pedir para que o mesmo realize login novamente, para isso temos rotas para refresh token.

Podemos melhorar nosso arquivo index.ts para que ele faça isso automaticamente durante as chamadas das rotas da sua aplicação.

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

Criamos uma função de refreshToken() que sempre será chamada antes de todas as chamadas da nossa aplicação ela iria verificar se expired do token já passou, e se sim já realizar uma nova chamada ao backend renovando o token e o expired. Lembrando essa lógica funciona de acordo com o backend e a rota de refresh por exemplo tem um tempo limite depois que passar do expired para se renovar o token ai seria mais regra de negócio.

Conclusão

Nesse post vimos cinco maneiras de melhorar nossa comunicação com o backend e levando em conta a melhor experiência para o usuário final, existem muitas outras abordagens que podem melhorar o nosso serviço de chamada ao backend, porém só em implementar esses conceitos já teremos uma melhor manutenção e usabilidade do nosso sistema. Em posts futuros veremos como melhorar ainda mais esse serviço.

Obrigado até a próxima

Referências

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