Consumindo uma API REST pública do zero

1. Fundamentos: O que é uma API REST e por que consumi-la?

API REST (Representational State Transfer) é um conjunto de regras que permite que sistemas se comuniquem via HTTP. Os conceitos fundamentais incluem:

  • Endpoints: URLs que representam recursos específicos (ex: /users, /posts)
  • Recursos: entidades manipuladas (usuários, posts, produtos)
  • Métodos HTTP: GET (ler), POST (criar), PUT (atualizar), DELETE (remover)

APIs públicas como JSONPlaceholder, OpenWeather e GitHub oferecem dados gratuitos para aprendizado e prototipagem. O fluxo básico é: o frontend faz uma requisição HTTP para a API, que processa e retorna dados em JSON, que são então renderizados na interface.

2. Configurando o ambiente de desenvolvimento

Primeiro, crie um projeto Node.js e instale as dependências necessárias:

mkdir api-consumer
cd api-consumer
npm init -y
npm install react react-dom vite @vitejs/plugin-react

Estrutura de pastas recomendada:

/src
  /services
    apiService.js
  /components
    UserList.jsx
    ErrorMessage.jsx
    LoadingSpinner.jsx
  App.jsx
  main.jsx

Configure o Vite com o arquivo vite.config.js:

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()]
});

3. Primeira requisição com Fetch API (puro JavaScript)

A Fetch API nativa do JavaScript é a forma mais simples de consumir APIs. Exemplo básico:

// Exemplo com async/await (recomendado)
async function getUsers() {
  try {
    const response = await fetch('https://jsonplaceholder.typicode.com/users');

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Erro na requisição:', error);
    throw error;
  }
}

// Uso
getUsers()
  .then(users => console.log(users))
  .catch(error => console.error(error));

O tratamento de erros é crucial: verifique response.ok e use try/catch para capturar exceções de rede.

4. Organizando o código em serviços reutilizáveis

Crie um módulo apiService.js que centraliza todas as requisições:

// services/apiService.js
const BASE_URL = 'https://jsonplaceholder.typicode.com';
const TIMEOUT = 10000; // 10 segundos

const defaultHeaders = {
  'Content-Type': 'application/json',
  // 'Authorization': 'Bearer token_aqui' // para APIs autenticadas
};

async function request(endpoint, options = {}) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), TIMEOUT);

  try {
    const response = await fetch(`${BASE_URL}${endpoint}`, {
      ...options,
      headers: {
        ...defaultHeaders,
        ...options.headers
      },
      signal: controller.signal
    });

    if (!response.ok) {
      const errorData = await response.json().catch(() => null);
      throw new Error(
        errorData?.message || `Erro ${response.status}: ${response.statusText}`
      );
    }

    return await response.json();
  } finally {
    clearTimeout(timeoutId);
  }
}

export const apiService = {
  get: (endpoint, options = {}) => request(endpoint, { ...options, method: 'GET' }),
  post: (endpoint, data, options = {}) => 
    request(endpoint, { ...options, method: 'POST', body: JSON.stringify(data) }),
  put: (endpoint, data, options = {}) => 
    request(endpoint, { ...options, method: 'PUT', body: JSON.stringify(data) }),
  delete: (endpoint, options = {}) => request(endpoint, { ...options, method: 'DELETE' })
};

5. Consumindo a API no React: hooks e estado

Agora, integre o serviço com componentes React usando hooks:

// components/UserList.jsx
import React, { useState, useEffect } from 'react';
import { apiService } from '../services/apiService';
import LoadingSpinner from './LoadingSpinner';
import ErrorMessage from './ErrorMessage';

function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchUsers = async () => {
      try {
        setLoading(true);
        setError(null);
        const data = await apiService.get('/users');
        setUsers(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchUsers();
  }, []);

  if (loading) return <LoadingSpinner />;
  if (error) return <ErrorMessage message={error} />;

  return (
    <div className="user-list">
      <h2>Lista de Usuários</h2>
      {users.map(user => (
        <div key={user.id} className="user-card">
          <h3>{user.name}</h3>
          <p>Email: {user.email}</p>
          <p>Cidade: {user.address.city}</p>
        </div>
      ))}
    </div>
  );
}

export default UserList;

6. Lidando com requisições assíncronas complexas

Para evitar problemas comuns como race conditions e requisições duplicadas:

// components/SearchableUserList.jsx
import React, { useState, useEffect, useRef } from 'react';
import { apiService } from '../services/apiService';

function SearchableUserList() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);
  const abortControllerRef = useRef(null);
  const cacheRef = useRef(new Map());

  useEffect(() => {
    if (!query.trim()) {
      setResults([]);
      return;
    }

    // Verificar cache
    if (cacheRef.current.has(query)) {
      setResults(cacheRef.current.get(query));
      return;
    }

    // Cancelar requisição anterior
    if (abortControllerRef.current) {
      abortControllerRef.current.abort();
    }

    const controller = new AbortController();
    abortControllerRef.current = controller;

    const fetchResults = async () => {
      setLoading(true);
      try {
        const data = await apiService.get(`/users?q=${query}`, {
          signal: controller.signal
        });

        // Armazenar em cache
        cacheRef.current.set(query, data);
        setResults(data);
      } catch (err) {
        if (err.name !== 'AbortError') {
          console.error('Erro na busca:', err);
        }
      } finally {
        setLoading(false);
      }
    };

    fetchResults();

    return () => {
      controller.abort(); // Cleanup: aborta se componente desmontar
    };
  }, [query]);

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="Buscar usuários..."
      />
      {loading && <p>Buscando...</p>}
      {results.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
}

7. Tratamento de erros e feedback ao usuário

Implemente componentes reutilizáveis para feedback visual:

// components/ErrorMessage.jsx
import React from 'react';

function ErrorMessage({ message, onRetry }) {
  return (
    <div className="error-message">
      <p>❌ Erro: {message}</p>
      {onRetry && (
        <button onClick={onRetry}>Tentar novamente</button>
      )}
    </div>
  );
}

// components/LoadingSpinner.jsx
function LoadingSpinner() {
  return (
    <div className="loading-spinner">
      <div className="spinner"></div>
      <p>Carregando...</p>
    </div>
  );
}

Para retry automático com exponential backoff:

async function fetchWithRetry(url, options = {}, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      return await response.json();
    } catch (error) {
      if (attempt === maxRetries) throw error;

      const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

8. Boas práticas e próximos passos

Custom hooks para reutilização:

// hooks/useApi.js
import { useState, useEffect, useCallback } from 'react';
import { apiService } from '../services/apiService';

export function useApi(endpoint, options = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  const fetchData = useCallback(async () => {
    try {
      setLoading(true);
      setError(null);
      const result = await apiService.get(endpoint, options);
      setData(result);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  }, [endpoint]);

  useEffect(() => {
    fetchData();
  }, [fetchData]);

  return { data, loading, error, refetch: fetchData };
}

Testando com Postman/Insomnia: Antes de codificar, teste os endpoints com ferramentas como Postman para entender a estrutura dos dados.

Considerações importantes:
- Autenticação: APIs privadas exigem tokens (JWT, API keys) no header Authorization
- CORS: Configure o backend para permitir requisições de origens específicas
- Documentação: Consulte sempre a documentação oficial da API para entender endpoints, parâmetros e limites de taxa

Com essas bases, você está pronto para consumir qualquer API REST pública e construir aplicações React robustas e bem estruturadas.

Referências