Cliente HTTP en TypeScript: solicitudes correctas y manejo de respuestas

Entrenador de Python en Línea para Principiantes

Aprende Python fácilmente sin sobrecargarte de teoría. Resuelve tareas prácticas con verificación automática, recibe pistas y escribe código directamente en el navegador, sin instalar nada.

Start Course

Introducción: por qué un cliente HTTP correcto es la base de una aplicación web moderna

Cualquier aplicación web moderna interactúa con el servidor a través de HTTP. Ya sea cargar una lista de productos, enviar un formulario de registro u obtener datos para un panel de control, todo se reduce a enviar solicitudes HTTP. En TypeScript, con su potente sistema de tipos, se puede construir no solo un cliente funcional, sino verdaderamente fiable y predecible.

En este artículo analizaremos cómo crear un cliente HTTP en TypeScript que maneje errores, tipifique correctamente las respuestas, gestione cabeceras y soporte middleware. Descubrirá por qué usar fetch sin envoltorio es una mala práctica y cómo convertirlo en una herramienta potente.



Fundamentos: de fetch sin procesar a un cliente tipificado

Empecemos con un ejemplo básico. El fetch integrado en JavaScript es un buen punto de partida, pero no proporciona una tipificación cómoda ni manejo de errores. En TypeScript podemos envolverlo en nuestra propia función.

Un envoltorio simple para fetch

Creemos una clase base HttpClient que acepte una URL base y configuraciones.

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>; }}

Ahora la llamada client.get<User>('/users/1') devolverá un objeto User tipificado. Esto ya es mucho mejor que trabajar con any.



Manejo avanzado de errores y tipificación de respuestas

Simplemente lanzar un error no es el mejor enfoque. Necesitamos distinguir entre errores de red, errores del servidor (4xx, 5xx) y errores de análisis JSON. Creemos nuestras propias clases de error.

Creación de clases de error

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'; }}

Manejo mejorado en el 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

Blogs

Book Recommendations