Cliente HTTP avançado em TypeScript: de requisições básicas a soluções industriais
No desenvolvimento de aplicações web modernas em TypeScript, o trabalho com requisições HTTP vai muito além de uma simples chamada fetch. Em nível avançado, é necessário não apenas enviar requisições, mas também tratar erros adequadamente, gerenciar tempos de espera, tipar respostas, implementar interceptadores e até mesmo armazenar dados em cache. Neste artigo, vamos abordar como construir um cliente HTTP robusto e flexível em TypeScript, adequado para projetos em produção.
1. Criação de um cliente HTTP tipado usando Genéricos (Generics)
Uma das principais vantagens do TypeScript é a tipagem estrita. Vamos criar uma classe base HttpClient que aceitará o tipo de resposta através de genéricos. Isso permitirá evitar erros em tempo de compilação.
// types.tsexport interface ApiResponse<T> { data: T; status: number; ok: boolean;}
export interface RequestConfig { headers?: Record<string, string>; params?: Record<string, string>; signal?: AbortSignal;}
// http-client.tsimport { ApiResponse, RequestConfig } from './types';
class HttpClient { private baseUrl: string;
constructor(baseUrl: string) { this.baseUrl = baseUrl; }
async get<T>(endpoint: string, config?: RequestConfig): Promise<ApiResponse<T>> { const url = new URL(`${this.baseUrl}${endpoint}`); if (config?.params) { Object.entries(config.params).forEach(([key, value]) => { url.searchParams.append(key, value); }); }
const response = await fetch(url.toString(), { method: 'GET', headers: { 'Content-Type': 'application/json', ...config?.headers, }, signal: config?.signal, });
const data: T = await response.json(); return { data, status: response.status, ok: response.ok, }; }
async post<T, U>(endpoint: string, body: U, config?: RequestConfig): Promise<ApiResponse<T>> { const response = await fetch(`${this.baseUrl}${endpoint}`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...config?.headers, }, body: JSON.stringify(body), signal: config?.signal, });
const data: T = await response.json(); return { data, status: response.status, ok: response.ok }; }}
export default HttpClient;2. Tratamento de erros e timeouts
Um cliente avançado deve tratar corretamente tanto erros de rede quanto erros de lógica de negócio. Vamos adicionar um mecanismo de timeout usando AbortController e tratamento personalizado de erros.
// errors.tsexport class HttpError extends Error { status: number; data?: unknown;
constructor(message: string, status: number, data?: unknown) { super(message); this.name = 'HttpError'; this.status = status; this.data = data; }}
export class TimeoutError extends Error { constructor(timeout: number) { super(`Requisição expirou após ${timeout}ms`); this.name = 'TimeoutError'; }}
// http-client.ts (complemento)async requestWithTimeout<T>( endpoint: string, options: RequestInit, timeout: number = 5000): Promise<ApiResponse<T>> { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout);
try { const response = await fetch(`${this.baseUrl}${endpoint}`, { ...options, signal: controller.signal, });
if (!response.ok) { const errorData = await response.json().catch(() => null); throw new HttpError( `HTTP ${response.status}: ${response.statusText}`, response.status, errorData ); }
const data: T = await response.json(); r