Voltar para Artigos
Front-end★ Destaque13 min de leitura

Autenticação e Interceptors com Axios no React

Como estruturar um cliente HTTP robusto com Axios: instância configurada, injeção automática de token, interceptor de resposta com retry, cancelamento de requisições e integração com React Query.

12 de agosto de 2026

Todo projeto React que consome uma API acaba com o mesmo problema: código de autenticação espalhado por dezenas de componentes. Cada useEffect com seu próprio Authorization header, cada catch com sua própria lógica de 401, cada componente replicando a URL base da API. O resultado é um código frágil e difícil de manter.

A solução é centralizar toda a lógica HTTP em uma instância única do Axios com Interceptors. Interceptors são middlewares do cliente HTTP — eles processam todas as requisições antes de enviá-las e todas as respostas antes de entregá-las ao componente, sem que o componente precise saber que isso acontece.

Por que Axios ao invés de fetch?

O fetch nativo é excelente para casos simples, mas o Axios tem vantagens estruturais para projetos maiores:

  • Interceptors — pipeline de transformação de request/response, impossível de replicar elegantemente com fetch
  • Cancelamento nativo — via AbortController, já integrado na API do Axios
  • Timeout por requisição — fetch não tem timeout nativo
  • Transformação automática de JSON — sem response.json() manual
  • Tipagem superior — AxiosResponse<T> e AxiosError<T> são tipados de forma mais granular

Estruturando a Instância Central

Crie um único arquivo src/lib/api.ts que exporta a instância configurada. Toda chamada de API no projeto importa daqui — nunca do axios diretamente.

api.ts
import axios, { type AxiosError, type InternalAxiosRequestConfig } from 'axios';

// Access Token em memória — nunca no localStorage (vulnerável a XSS)
let accessToken: string | null = null;

export function setAccessToken(token: string): void {
  accessToken = token;
}

export function clearAccessToken(): void {
  accessToken = null;
}

export const api = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:3333',
  timeout: 10_000, // 10 segundos de timeout global
  headers: {
    'Content-Type': 'application/json',
  },
  // Necessário para enviar/receber HttpOnly Cookies (Refresh Token)
  withCredentials: true,
});

Request Interceptor: Injetando o Token

O request interceptor roda antes de cada requisição sair. Injeta o Access Token em memória no header Authorization:

api.ts (continuação)
api.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    if (accessToken) {
      config.headers.Authorization = `Bearer ${accessToken}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

Response Interceptor: Renovação de Token com Fila

O response interceptor é a parte mais complexa. Ele precisa lidar com 401, renovar o token e repetir a requisição original — tudo de forma transparente. O desafio é evitar múltiplas chamadas de refresh simultâneas quando várias requisições falham ao mesmo tempo:

api.ts (continuação)
type FailedRequest = {
  resolve: (token: string) => void;
  reject: (err: AxiosError) => void;
};

let isRefreshing = false;
let failedRequestsQueue: Array<FailedRequest> = [];

api.interceptors.response.use(
  // Resposta bem-sucedida: passa direto
  (response) => response,

  // Resposta com erro
  async (error: AxiosError<{ code?: string }>) => {
    const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };

    // Só trata 401 e evita loop infinito
    if (error.response?.status !== 401 || originalRequest._retry) {
      return Promise.reject(error);
    }

    // Verifica se é token expirado (o backend deve retornar um código)
    const isTokenExpired = error.response.data?.code === 'token.expired';
    if (!isTokenExpired) {
      // 401 por outra razão (ex: sem permissão) — não tenta renovar
      clearAccessToken();
      window.location.href = '/login';
      return Promise.reject(error);
    }

    if (isRefreshing) {
      // Já está renovando: coloca na fila e aguarda
      return new Promise<string>((resolve, reject) => {
        failedRequestsQueue.push({ resolve, reject });
      })
        .then((newToken) => {
          originalRequest.headers.Authorization = `Bearer ${newToken}`;
          return api(originalRequest);
        })
        .catch((err) => Promise.reject(err));
    }

    originalRequest._retry = true;
    isRefreshing = true;

    try {
      // O Refresh Token é enviado automaticamente via HttpOnly Cookie
      const { data } = await api.post<{ accessToken: string }>('/auth/refresh');
      const newToken = data.accessToken;

      setAccessToken(newToken);

      // Resolve todas as requisições que estavam aguardando
      failedRequestsQueue.forEach(({ resolve }) => resolve(newToken));
      failedRequestsQueue = [];

      // Repete a requisição original com o novo token
      originalRequest.headers.Authorization = `Bearer ${newToken}`;
      return api(originalRequest);
    } catch (refreshError) {
      // Refresh falhou: rejeita todas as requisições na fila e desloga
      failedRequestsQueue.forEach(({ reject }) => reject(refreshError as AxiosError));
      failedRequestsQueue = [];
      clearAccessToken();
      window.location.href = '/login';
      return Promise.reject(refreshError);
    } finally {
      isRefreshing = false;
    }
  }
);

Cancelamento de Requisições com AbortController

Um problema comum em React é a requisição que retorna depois que o componente foi desmontado — o temido Warning: Can't perform a React state update on an unmounted component. A solução correta é cancelar a requisição no cleanup do useEffect:

useUsers.ts
import { useState, useEffect } from 'react';
import { api } from '../lib/api';
import type { CanceledError } from 'axios';

interface User {
  id: string;
  name: string;
  email: string;
}

export function useUsers() {
  const [users, setUsers] = useState<Array<User>>([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const controller = new AbortController();

    async function fetchUsers() {
      try {
        setIsLoading(true);
        const { data } = await api.get<Array<User>>('/users', {
          // Vincula o AbortController ao Axios
          signal: controller.signal,
        });
        setUsers(data);
      } catch (err) {
        // Ignora erros de cancelamento — são intencionais
        if ((err as CanceledError).code === 'ERR_CANCELED') return;
        setError('Falha ao carregar usuários.');
      } finally {
        setIsLoading(false);
      }
    }

    fetchUsers();

    return () => {
      // Cancela a requisição quando o componente desmonta
      // ou quando o useEffect re-executa (dependências mudaram)
      controller.abort();
    };
  }, []);

  return { users, isLoading, error };
}

Se você usa React Query ou SWR, eles gerenciam o cancelamento automaticamente. O cancelamento manual com AbortController é necessário apenas quando você usa useEffect diretamente para fazer fetching — o que geralmente é um sinal de que você deveria usar React Query.

Integração com React Query

A combinação mais poderosa é usar a instância do Axios dentro do React Query. O React Query cuida do cache, loading states, refetch e invalidação. O Axios cuida dos interceptors de autenticação:

useUsers.ts (com React Query)
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '../lib/api';

interface User {
  id: string;
  name: string;
  email: string;
}

// Query Keys como constantes — evita strings mágicas espalhadas
export const userKeys = {
  all: ['users'] as const,
  detail: (id: string) => ['users', id] as const,
};

// Hook de leitura
export function useUsers() {
  return useQuery({
    queryKey: userKeys.all,
    queryFn: async ({ signal }) => {
      const { data } = await api.get<Array<User>>('/users', { signal });
      return data;
    },
    staleTime: 5 * 60 * 1000, // 5 minutos antes de refetch
  });
}

// Hook de mutação
export function useCreateUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (newUser: Omit<User, 'id'>) => {
      const { data } = await api.post<User>('/users', newUser);
      return data;
    },
    onSuccess: () => {
      // Invalida o cache da lista após criar um usuário
      queryClient.invalidateQueries({ queryKey: userKeys.all });
    },
  });
}

Tipando Erros do Axios

Um ponto que muitos desenvolvedores erram: o tipo do erro no catch do Axios é unknown, não AxiosError. Use o type guard fornecido pelo próprio Axios:

handleApiError.ts
import { isAxiosError } from 'axios';

// Tipo do corpo de erro que a API retorna
interface ApiError {
  message: string;
  code?: string;
  field?: string;
}

export function handleApiError(error: unknown): string {
  if (isAxiosError<ApiError>(error)) {
    // Erro com resposta do servidor (4xx, 5xx)
    if (error.response) {
      return error.response.data.message ?? 'Erro no servidor.';
    }
    // Sem resposta: timeout, rede, CORS
    if (error.request) {
      return 'Sem resposta do servidor. Verifique sua conexão.';
    }
  }
  // Erro inesperado (bug no código)
  return 'Erro inesperado. Tente novamente.';
}

// Uso:
// try {
//   await api.post('/users', data);
// } catch (err) {
//   toast.error(handleApiError(err));
// }

Conclusão

Uma instância Axios bem configurada é a fundação de um frontend escalável. Com ela, seus componentes e hooks ficam livres de lógica de autenticação — eles simplesmente fazem chamadas de API e recebem dados tipados. Toda a complexidade de injeção de token, renovação transparente, cancelamento e tratamento de erro vive em um único lugar, testável de forma isolada.