Introdução: por que um cliente HTTP correto é a base de uma aplicação web moderna
Qualquer aplicação web moderna interage com o servidor via HTTP. Seja carregando uma lista de produtos, enviando um formulário de registro ou obtendo dados para um dashboard — tudo se resume ao envio de requisições HTTP. Com TypeScript, com seu poderoso sistema de tipos, é possível construir não apenas um cliente funcional, mas um cliente HTTP verdadeiramente confiável e previsível.
Neste artigo, vamos analisar como criar um cliente HTTP em TypeScript que tratará erros, tipará respostas corretamente, gerenciará cabeçalhos e suportará middleware. Você aprenderá por que usar fetch puro sem um wrapper é uma má prática e como transformá-lo em uma ferramenta poderosa.
Fundamentos: do fetch puro ao cliente tipado
Vamos começar com um exemplo básico. O fetch embutido no JavaScript é um bom ponto de partida, mas ele não fornece tipagem conveniente nem tratamento de erros. Com TypeScript, podemos encapsulá-lo em nossa própria função.
Wrapper simples para fetch
Vamos criar uma classe base HttpClient que aceitará uma URL base e configurações.
interface RequestOptions { headers?: Record<string, string>; params?: Record<string, string>;}
class HttpClient { private baseUrl: string; private defaultHeaders: Record<string, string>;
constructor(baseUrl: string, defaultHeaders: Record<string, string> = {}) { this.baseUrl = baseUrl; this.defaultHeaders = defaultHeaders; }
private buildUrl(path: string, params?: Record<string, string>): string { const url = new URL(`${this.baseUrl}${path}`); if (params) { Object.entries(params).forEach(([key, value]) => url.searchParams.append(key, value)); } return url.toString(); }
async get<T>(path: string, options?: RequestOptions): Promise<T> { const url = this.buildUrl(path, options?.params); const response = await fetch(url, { method: 'GET', headers: { ...this.defaultHeaders, ...options?.headers }, });
if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); }
return response.json() as Promise<T>; }}Agora, a chamada client.get<User>('/users/1') retornará um objeto User tipado. Isso já é muito melhor do que trabalhar com any.
Tratamento avançado de erros e tipagem de respostas
Simplesmente lançar um erro não é a melhor abordagem. Precisamos distinguir entre erros de rede, erros do servidor (4xx, 5xx) e erros de parsing de JSON. Vamos criar nossas próprias classes de erro.
Criação de classes de erro
class HttpError extends Error { status: number; body: any;
constructor(message: string, status: number, body?: any) { super(message); this.name = 'HttpError'; this.status = status; this.body = body; }}
class NetworkError extends Error { constructor(message: string) { super(message); this.name = 'NetworkError'; }}
class ParseError extends Error { constructor(message: string) { super(message); this.name = 'ParseError'; }}Tratamento aprimorado no método get
async get<T>(path: string, options?: RequestOptions): Promise<T> { try { const url = this.buildUrl(path, options?.params); const response = await fetch(url, { method: 'GET', headers: { ...this.defaultHeaders, ...options?.headers }, });
let body: any; try { body = await response.json(); } catch { throw new ParseError('Failed to parse response as JSON'); }
if (!response.ok) { throw new HttpError( `Request failed with status ${response.status}`, response.status, body ); }
return body as T; } catch (er