Consumo de APIs em React Native: HTTP, autenticação e tratamento robusto de erros

Capítulo 10

Tempo estimado de leitura: 14 minutos

+ Exercício

Vise3o geral: o que significa “consumir APIs” em um app React Native

Consumir uma API e9 transformar requisie7f5es HTTP (GET, POST, PUT, DELETE etc.) em dados utilize1veis no app. Em apps reais, isso envolve mais do que “fazer um fetch”: vocea precisa lidar com autenticae7e3o, timeouts, cancelamento, padronizae7e3o de erros, retries, logs e estados de carregamento para manter a experieancia consistente e diagnf3sticos possedveis.

Neste capedtulo, vocea vai montar uma camada de rede reutilize1vel, com duas abordagens: fetch (nativo) e Axios (biblioteca), e depois evoluir para autenticae7e3o com token, refresh e protee7e3o de rotas.

HTTP na pre1tica: fetch vs Axios

Quando usar fetch

  • Sem dependeancias externas.
  • Bom para apps simples ou quando vocea quer controle total.
  • Vocea precisa implementar manualmente: timeout, interceptae7e3o, padronizae7e3o de erros e retries.

Quando usar Axios

  • Interceptors para request/response.
  • Timeout nativo.
  • Cancelamento e integrae7e3o com AbortController.
  • Transformae7e3o de resposta e headers mais pre1ticos.

Passo a passo: criando um client HTTP reutilize1vel

O objetivo e9 ter um fanico ponto para: baseURL, headers, token, tratamento de erros, logs e retry. Assim, suas telas chamam fune7f5es de servie7o (ex.: authService.login, userService.me) sem duplicar lf3gica.

1) Defina tipos de resposta e erro padronizados

Padronizar evita que cada tela trate erros de um jeito. Um formato comum e9: sucesso retorna { ok: true, data }, falha retorna { ok: false, error }.

export type ApiSuccess<T> = { ok: true; data: T; status: number };export type ApiFailure = {  ok: false;  status?: number;  error: {    code: string;    message: string;    details?: unknown;    isNetworkError?: boolean;    isTimeout?: boolean;    isAuthError?: boolean;  };};export type ApiResult<T> = ApiSuccess<T> | ApiFailure;

Exemplos de code: NETWORK_ERROR, TIMEOUT, UNAUTHORIZED, VALIDATION_ERROR, UNKNOWN.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

2) Implementae7e3o com fetch (com timeout e cancelamento)

O fetch ne3o tem timeout por padre3o. Use AbortController para cancelar por timeout e tambe9m quando a tela desmontar.

const BASE_URL = 'https://api.seudominio.com';type FetchOptions = RequestInit & { timeoutMs?: number };function buildUrl(path: string) {  return path.startsWith('http') ? path : `${BASE_URL}${path}`;}async function fetchJson<T>(path: string, options: FetchOptions = {}): Promise<ApiResult<T>> {  const { timeoutMs = 15000, ...init } = options;  const controller = new AbortController();  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);  try {    const res = await fetch(buildUrl(path), {      ...init,      signal: controller.signal,      headers: {        'Content-Type': 'application/json',        ...(init.headers || {}),      },    });    const status = res.status;    const contentType = res.headers.get('content-type') || '';    const isJson = contentType.includes('application/json');    const body = isJson ? await res.json().catch(() => null) : await res.text().catch(() => null);    if (!res.ok) {      const message = (body && (body.message || body.error)) || 'Ne3o foi possedvel completar a requisie7e3o.';      const code = status === 401 ? 'UNAUTHORIZED' : status === 422 ? 'VALIDATION_ERROR' : 'HTTP_ERROR';      return {        ok: false,        status,        error: {          code,          message,          details: body,          isAuthError: status === 401,        },      };    }    return { ok: true, data: body as T, status };  } catch (e: any) {    const isAbort = e?.name === 'AbortError';    if (isAbort) {      return {        ok: false,        error: { code: 'TIMEOUT', message: 'A requisie7e3o demorou demais. Tente novamente.', isTimeout: true },      };    }    return {      ok: false,      error: {        code: 'NETWORK_ERROR',        message: 'Sem conexe3o ou servidor indisponedvel.',        details: String(e),        isNetworkError: true,      },    };  } finally {    clearTimeout(timeoutId);  }}

Para cancelar manualmente (ex.: ao sair da tela), vocea pode expor o AbortController ou aceitar um signal externo. Em apps reais, e9 comum o service receber um signal opcional.

3) Implementae7e3o com Axios (client com interceptors)

Com Axios, vocea cria uma inste2ncia e centraliza: baseURL, timeout, headers e interceptors para token, logs e normalizae7e3o de erros.

import axios, { AxiosError, AxiosInstance } from 'axios';const api: AxiosInstance = axios.create({  baseURL: 'https://api.seudominio.com',  timeout: 15000,  headers: { 'Content-Type': 'application/json' },});function normalizeAxiosError(err: unknown): ApiFailure {  const e = err as AxiosError<any>;  if (e.code === 'ECONNABORTED') {    return { ok: false, error: { code: 'TIMEOUT', message: 'A requisie7e3o demorou demais. Tente novamente.', isTimeout: true } };  }  if (!e.response) {    return { ok: false, error: { code: 'NETWORK_ERROR', message: 'Sem conexe3o ou servidor indisponedvel.', details: e.message, isNetworkError: true } };  }  const status = e.response.status;  const data = e.response.data;  const message = data?.message || data?.error || 'Ne3o foi possedvel completar a requisie7e3o.';  const code = status === 401 ? 'UNAUTHORIZED' : status === 422 ? 'VALIDATION_ERROR' : 'HTTP_ERROR';  return { ok: false, status, error: { code, message, details: data, isAuthError: status === 401 } };}export async function request<T>(fn: () => Promise<{ data: T; status: number }>): Promise<ApiResult<T>> {  try {    const res = await fn();    return { ok: true, data: res.data, status: res.status };  } catch (err) {    return normalizeAxiosError(err);  }}export { api };

4) Interceptors: token, logs e correlae7e3o de requisie7f5es

Interceptors permitem anexar o token automaticamente e registrar informae7f5es fateis para diagnf3stico (sem vazar dados sensedveis).

import { api } from './apiClient';import { authStore } from '../stores/authStore';api.interceptors.request.use((config) => {  const token = authStore.getAccessToken();  if (token) config.headers.Authorization = `Bearer ${token}`;  config.headers['X-Request-Id'] = `${Date.now()}-${Math.random().toString(16).slice(2)}`;  if (__DEV__) {    console.log('[HTTP] =>', config.method?.toUpperCase(), config.url);  }  return config;});api.interceptors.response.use(  (response) => {    if (__DEV__) {      console.log('[HTTP] <=', response.status, response.config.url);    }    return response;  },  (error) => {    if (__DEV__) {      const status = error?.response?.status;      console.log('[HTTP] <= ERROR', status, error?.config?.url);    }    return Promise.reject(error);  }

Boas pre1ticas de log: ne3o imprimir token, ne3o imprimir payloads sensedveis (senha, documentos), e preferir logs apenas em desenvolvimento.

Cancelamento de requisie7f5es (evitando setState apf3s unmount)

Cancelamento e9 importante para: busca em tempo real, troca re1pida de telas e evitar atualizae7f5es de estado quando o componente je1 ne3o existe.

Com Axios + AbortController

import { api, request } from './apiClient';export async function searchUsers(q: string, signal?: AbortSignal) {  return request<{ id: string; name: string }[]>(() =>    api.get('/users', { params: { q }, signal })  );}

Uso em uma tela (exemplo simplificado):

import { useEffect, useState } from 'react';import { searchUsers } from '../services/userService';export function useUserSearch(query: string) {  const [loading, setLoading] = useState(false);  const [data, setData] = useState<any[]>([]);  const [error, setError] = useState<string | null>(null);  useEffect(() => {    if (!query) return;    const controller = new AbortController();    setLoading(true);    setError(null);    searchUsers(query, controller.signal).then((res) => {      if (res.ok) setData(res.data);      else setError(res.error.message);    }).finally(() => setLoading(false));    return () => controller.abort();  }, [query]);  return { loading, data, error };}

Retries com backoff (quando e como aplicar)

Retry e9 fatil para falhas transitf3rias (rede inste1vel, 502/503/504). Ne3o e9 recomendado repetir automaticamente em erros de validae7e3o (422) ou autenticae7e3o (401) sem uma estrate9gia clara.

Implementando retry gene9rico para ApiResult

function sleep(ms: number) {  return new Promise((r) => setTimeout(r, ms));}type RetryOptions = {  retries: number;  baseDelayMs?: number;  shouldRetry?: (result: ApiFailure) => boolean;};export async function withRetry<T>(  task: () => Promise<ApiResult<T>>,  opts: RetryOptions): Promise<ApiResult<T>> {  const { retries, baseDelayMs = 400, shouldRetry } = opts;  for (let attempt = 0; attempt <= retries; attempt++) {    const res = await task();    if (res.ok) return res;    const canRetry = shouldRetry      ? shouldRetry(res)      : !!(res.error.isNetworkError || res.error.isTimeout || (res.status && [502, 503, 504].includes(res.status)));    if (!canRetry || attempt === retries) return res;    const delay = baseDelayMs * Math.pow(2, attempt);    await sleep(delay);  }  return { ok: false, error: { code: 'UNKNOWN', message: 'Falha inesperada.' } };}

Exemplo de uso:

import { api, request } from './apiClient';import { withRetry } from './retry';export function getMe() {  return withRetry(    () => request(() => api.get('/me')),    { retries: 2 }  );}

Autenticae7e3o com token: login, refresh e armazenamento seguro

Modelo comum de autenticae7e3o

  • Access token: curto (minutos). Vai em Authorization: Bearer ....
  • Refresh token: mais longo (dias). Usado para obter novo access token quando expirar.

Nem toda API usa refresh token. Quando ne3o houver, a estrate9gia e9 redirecionar para login ao receber 401.

Armazenamento seguro do token

Evite guardar token em armazenamento ne3o seguro. Em React Native, use um cofre seguro (Keychain/Keystore) via biblioteca apropriada. Abaixo, um exemplo com uma API gene9rica de “secure storage” (substitua pela biblioteca escolhida no projeto).

// secureStorage.ts (interface simples para isolar a dependeancia)export const secureStorage = {  async set(key: string, value: string) {    // implementar com Keychain/Keystore via biblioteca    // ex.: setGenericPassword(key, value) ou equivalente  },  async get(key: string) {    // retornar string | null  },  async remove(key: string) {    // remover do cofre  },};

AuthStore: fonte fanica do token em memf3ria

Mantenha o token em memf3ria para uso re1pido e sincronize com o armazenamento seguro para persisteancia.

type AuthState = {  accessToken: string | null;  refreshToken: string | null;};class AuthStore {  private state: AuthState = { accessToken: null, refreshToken: null };  getAccessToken() { return this.state.accessToken; }  getRefreshToken() { return this.state.refreshToken; }  setTokens(tokens: { accessToken: string; refreshToken?: string | null }) {    this.state.accessToken = tokens.accessToken;    if (typeof tokens.refreshToken !== 'undefined') {      this.state.refreshToken = tokens.refreshToken;    }  }  clear() {    this.state = { accessToken: null, refreshToken: null };  }}export const authStore = new AuthStore();

Servie7o de autenticae7e3o: login e refresh

import { api, request } from '../http/apiClient';import { secureStorage } from '../storage/secureStorage';import { authStore } from '../stores/authStore';type LoginResponse = { accessToken: string; refreshToken?: string };export async function login(email: string, password: string) {  const result = await request<LoginResponse>(() => api.post('/auth/login', { email, password }));  if (result.ok) {    authStore.setTokens({ accessToken: result.data.accessToken, refreshToken: result.data.refreshToken ?? null });    await secureStorage.set('accessToken', result.data.accessToken);    if (result.data.refreshToken) await secureStorage.set('refreshToken', result.data.refreshToken);  }  return result;}export async function refresh() {  const refreshToken = authStore.getRefreshToken() || (await secureStorage.get('refreshToken'));  if (!refreshToken) {    return { ok: false, error: { code: 'UNAUTHORIZED', message: 'Sesse3o expirada. Fae7a login novamente.', isAuthError: true } } as const;  }  const result = await request<LoginResponse>(() => api.post('/auth/refresh', { refreshToken }));  if (result.ok) {    authStore.setTokens({ accessToken: result.data.accessToken, refreshToken: result.data.refreshToken ?? refreshToken });    await secureStorage.set('accessToken', result.data.accessToken);    if (result.data.refreshToken) await secureStorage.set('refreshToken', result.data.refreshToken);  }  return result;}export async function logout() {  authStore.clear();  await secureStorage.remove('accessToken');  await secureStorage.remove('refreshToken');}

Refresh autome1tico com interceptor (tratando 401 de forma robusta)

Quando a API retornar 401 por token expirado, vocea pode tentar refresh e repetir a requisie7e3o original. O cuidado principal e9 evitar “tempestade” de refresh: ve1rias requisie7f5es falham ao mesmo tempo e todas tentam refresh. A solue7e3o e9 enfileirar enquanto um refresh este1 em andamento.

import { api } from './apiClient';import { refresh, logout } from '../services/authService';import { authStore } from '../stores/authStore';let isRefreshing = false;let pending: Array<(token: string | null) => void> = [];function subscribe(cb: (token: string | null) => void) {  pending.push(cb);}function notify(token: string | null) {  pending.forEach((cb) => cb(token));  pending = [];}api.interceptors.response.use(  (res) => res,  async (error) => {    const original = error.config;    const status = error?.response?.status;    if (status !== 401 || original?._retry) {      return Promise.reject(error);    }    original._retry = true;    if (isRefreshing) {      return new Promise((resolve, reject) => {        subscribe((token) => {          if (!token) return reject(error);          original.headers.Authorization = `Bearer ${token}`;          resolve(api(original));        });      });    }    isRefreshing = true;    const ref = await refresh();    isRefreshing = false;    if (!ref.ok) {      notify(null);      await logout();      return Promise.reject(error);    }    const newToken = authStore.getAccessToken();    notify(newToken);    original.headers.Authorization = `Bearer ${newToken}`;    return api(original);  }

Observae7e3o: o campo _retry e9 um marcador para evitar loop infinito. Em TypeScript, vocea pode estender o tipo de config para permitir esse campo.

Protee7e3o de rotas (acesso autenticado vs pfablico)

A protee7e3o de rotas consiste em renderizar pilhas/abas diferentes dependendo do estado de autenticae7e3o. A regra e9 simples: se existe token ve1lido, mostra e1rea logada; caso contre1rio, mostra login/cadastro.

Passo a passo: bootstrap do token ao abrir o app

  • Ao iniciar, carregue tokens do armazenamento seguro.
  • Coloque em memf3ria (authStore/estado global).
  • Se houver refresh token e o access token estiver ausente/expirado, tente refresh.
import { useEffect, useState } from 'react';import { secureStorage } from '../storage/secureStorage';import { authStore } from '../stores/authStore';import { refresh } from '../services/authService';export function useAuthBootstrap() {  const [ready, setReady] = useState(false);  const [isSignedIn, setIsSignedIn] = useState(false);  useEffect(() => {    (async () => {      const accessToken = await secureStorage.get('accessToken');      const refreshToken = await secureStorage.get('refreshToken');      if (accessToken) authStore.setTokens({ accessToken, refreshToken: refreshToken ?? null });      if (!accessToken && refreshToken) {        const ref = await refresh();        if (ref.ok) {          setIsSignedIn(true);          setReady(true);          return;        }      }      setIsSignedIn(!!authStore.getAccessToken());      setReady(true);    })();  }, []);  return { ready, isSignedIn };}

Na e1rvore de navegae7e3o, vocea decide qual stack renderizar com base em ready e isSignedIn. Enquanto ready for falso, mostre uma tela de splash/carregamento.

Estados de carregamento e mensagens amige1veis

Uma experieancia consistente depende de estados previsedveis:

  • loading: exibir indicador e desabilitar botf5es para evitar requisie7f5es duplicadas.
  • success: renderizar dados.
  • empty: renderizar estado vazio quando a lista vier vazia.
  • error: mostrar mensagem amige1vel e ae7e3o de tentar novamente.

Mapeando erros te9cnicos para mensagens de UI

import type { ApiFailure } from '../http/types';export function toUserMessage(failure: ApiFailure): string {  switch (failure.error.code) {    case 'NETWORK_ERROR':      return 'Sem conexe3o. Verifique sua internet e tente novamente.';    case 'TIMEOUT':      return 'Demorou mais do que o esperado. Tente novamente.';    case 'UNAUTHORIZED':      return 'Sua sesse3o expirou. Entre novamente.';    case 'VALIDATION_ERROR':      return 'Revise os dados informados.';    default:      return failure.error.message || 'Ocorreu um erro. Tente novamente.';  }}

Evite requisie7f5es duplicadas

Em ae7f5es de bote3o (ex.: login), desabilite enquanto loading for verdadeiro. Em listas com pull-to-refresh, ignore refresh se je1 estiver carregando.

Logs e diagnf3stico: o que registrar e como

Logs ajudam a entender falhas em produe7e3o, mas precisam ser seguros. Registre:

  • Me9todo, URL (sem query sensedvel), status.
  • Um X-Request-Id para correlacionar com logs do backend.
  • Tempo de resposta (lateancia).
  • Cf3digo de erro padronizado (NETWORK_ERROR, HTTP_ERROR etc.).

Ne3o registre: token, senha, refresh token, dados pessoais sensedveis.

Medindo lateancia com interceptor

api.interceptors.request.use((config) => {  (config as any).metadata = { start: Date.now() };  return config;});api.interceptors.response.use(  (res) => {    const start = (res.config as any).metadata?.start;    const ms = start ? Date.now() - start : undefined;    if (__DEV__) console.log('[HTTP] time(ms)=', ms, res.config.url);    return res;  },  (err) => {    const start = (err.config as any)?.metadata?.start;    const ms = start ? Date.now() - start : undefined;    if (__DEV__) console.log('[HTTP] time(ms)=', ms, err.config?.url);    return Promise.reject(err);  }

Organizando a camada de servie7os (padre3o recomendado)

Uma organizae7e3o simples e escale1vel:

  • src/http/: client (Axios), normalizae7e3o de erros, retry, tipos.
  • src/services/: fune7f5es por domednio (auth, users, orders).
  • src/storage/: secure storage (isolando dependeancias).
  • src/stores/: estado de autenticae7e3o em memf3ria.

Exemplo de service de domednio

import { api, request } from '../http/apiClient';type Me = { id: string; name: string; email: string };export function getMyProfile() {  return request<Me>(() => api.get('/me'));}type UpdateMeInput = { name: string };export function updateMyProfile(input: UpdateMeInput) {  return request<Me>(() => api.put('/me', input));}

Checklist de robustez para consumo de APIs

ItemObjetivoComo implementar
TimeoutEvitar travas e esperas infinitastimeout no Axios ou AbortController no fetch
CancelamentoEvitar corrida de respostas e setState apf3s unmountAbortController e cleanup no useEffect
Padronizae7e3o de errosUI consistente e menos ifsApiResult + normalizeAxiosError
InterceptorsToken, logs e refresh centralizadosapi.interceptors
Refresh com filaEvitar mfaltiplos refresh simulte2neosisRefreshing + subscribers
Retry com backoffResilieancia a falhas transitf3riaswithRetry com regras
Mensagens amige1veisMelhor UX em falhastoUserMessage por cf3digo
Logs segurosDiagnf3stico sem vazamentoRequest-id, status, lateancia; sem tokens

Agora responda o exercício sobre o conteúdo:

Ao projetar uma camada de rede reutilizável em React Native, qual prática ajuda a manter a UI consistente e reduzir lógica duplicada nas telas ao lidar com falhas de requisição?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Ao usar um formato padronizado (ex.: ApiResult) e normalizar erros, as telas consomem sempre a mesma estrutura e a UI pode exibir mensagens consistentes com menos condicionais e menos duplicação.

Próximo capitúlo

Armazenamento local em React Native: AsyncStorage, dados sensíveis e cache

Arrow Right Icon
Capa do Ebook gratuito React Native Essencial: criando apps completos com JavaScript e boas práticas
63%

React Native Essencial: criando apps completos com JavaScript e boas práticas

Novo curso

16 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.