CAP 06 · LEC 07·Asincronía

fetch y APIs: conecta tu app con el mundo

fetch es la API nativa del browser y Node.js para hacer peticiones HTTP. Es la base de cualquier app que consuma datos externos — y combinada con async/await, el código queda limpio y legible.

● INTERMEDIO10 min lectura5 ejerciciospor Fernando Herrera · actualizado mayo de 2026
¿Encontraste un error o algo que mejorar?Editá esta lección en GitHub →

fetch — la API nativa para HTTP

fetch devuelve una Promise que resuelve con un objeto Response. Necesitas un segundo await para obtener el cuerpo:

// fetch devuelve una Promise<Response> // response.json() devuelve otra Promise con el cuerpo parseado async function obtenerPost(id) { const respuesta = await fetch( `https://jsonplaceholder.typicode.com/posts/${id}` ); const post = await respuesta.json(); console.log(post.title); } obtenerPost(1); // "sunt aut facere repellat provident..." // La Response también tiene .text(), .blob(), .arrayBuffer() async function obtenerTexto(url) { const respuesta = await fetch(url); const texto = await respuesta.text(); return texto; }
type Post = { id: number; userId: number; title: string; body: string; }; async function obtenerPost(id: number): Promise<Post> { const respuesta: Response = await fetch( `https://jsonplaceholder.typicode.com/posts/${id}` ); const post: Post = await respuesta.json(); console.log(post.title); return post; } obtenerPost(1); // "sunt aut facere repellat provident..."
Salidasunt aut facere repellat provident...
fetch no lanza por errores HTTP

fetch solo rechaza la Promise si hay un error de red (sin conexión, CORS). Un status 404 o 500 resuelve la Promise con response.ok === false. Siempre verifica response.ok.

GET: obtener datos de una API

El patrón completo para un GET con manejo de errores:

async function obtenerUsuarios() { const respuesta = await fetch("https://jsonplaceholder.typicode.com/users"); if (!respuesta.ok) { throw new Error(`Error HTTP ${respuesta.status}: ${respuesta.statusText}`); } const usuarios = await respuesta.json(); // Transformar los datos al formato que necesitamos return usuarios.map(usuario => ({ id: usuario.id, nombre: usuario.name, email: usuario.email, ciudad: usuario.address.city, })); } async function mostrarUsuarios() { try { const usuarios = await obtenerUsuarios(); usuarios.forEach(u => { console.log(`${u.nombre}${u.ciudad}`); }); } catch (err) { console.error("Error al cargar usuarios:", err.message); } } mostrarUsuarios();
type UsuarioApi = { id: number; name: string; email: string; address: { city: string }; }; type UsuarioResumen = { id: number; nombre: string; email: string; ciudad: string; }; async function obtenerUsuarios(): Promise<UsuarioResumen[]> { const respuesta = await fetch("https://jsonplaceholder.typicode.com/users"); if (!respuesta.ok) { throw new Error(`Error HTTP ${respuesta.status}: ${respuesta.statusText}`); } const usuarios: UsuarioApi[] = await respuesta.json(); return usuarios.map(usuario => ({ id: usuario.id, nombre: usuario.name, email: usuario.email, ciudad: usuario.address.city, })); } async function mostrarUsuarios(): Promise<void> { try { const usuarios = await obtenerUsuarios(); usuarios.forEach(u => { console.log(`${u.nombre}${u.ciudad}`); }); } catch (err) { if (err instanceof Error) { console.error("Error al cargar usuarios:", err.message); } } } mostrarUsuarios();
SalidaLeanne Graham — Gwenborough Ervin Howell — Wisokyburgh ...

POST: enviar datos a una API

Para enviar datos, necesitas configurar el método, los headers y el body:

async function crearPost(titulo, contenido, autorId) { const respuesta = await fetch("https://jsonplaceholder.typicode.com/posts", { method: "POST", headers: { "Content-Type": "application/json", // Authorization: `Bearer ${token}` // si la API requiere auth }, body: JSON.stringify({ title: titulo, body: contenido, userId: autorId, }), }); if (!respuesta.ok) { const errorBody = await respuesta.text(); throw new Error(`Error ${respuesta.status}: ${errorBody}`); } const postCreado = await respuesta.json(); console.log("Post creado con ID:", postCreado.id); // 101 return postCreado; } crearPost("Mi primer post", "Contenido del post", 1);
type NuevoPost = { titulo: string; contenido: string; autorId: number; }; type PostCreado = NuevoPost & { id: number }; async function crearPost(datos: NuevoPost): Promise<PostCreado> { const respuesta = await fetch("https://jsonplaceholder.typicode.com/posts", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ title: datos.titulo, body: datos.contenido, userId: datos.autorId, }), }); if (!respuesta.ok) { const errorBody = await respuesta.text(); throw new Error(`Error ${respuesta.status}: ${errorBody}`); } const postCreado: PostCreado = await respuesta.json(); console.log("Post creado con ID:", postCreado.id); // 101 return postCreado; } crearPost({ titulo: "Mi primer post", contenido: "Contenido", autorId: 1 });
SalidaPost creado con ID: 101

Manejo de errores HTTP

Los errores HTTP tienen semántica — un 404 no es lo mismo que un 500. Tu código debe distinguirlos:

class ErrorHttp extends Error { constructor(status, mensaje) { super(mensaje); this.name = "ErrorHttp"; this.status = status; } } async function peticionSegura(url, opciones = {}) { let respuesta; try { respuesta = await fetch(url, opciones); } catch (err) { // Error de red: sin conexión, CORS, DNS throw new Error(`Error de red: ${err.message}`); } if (respuesta.status === 401) { throw new ErrorHttp(401, "No autenticado — inicia sesión"); } if (respuesta.status === 403) { throw new ErrorHttp(403, "Sin permisos para este recurso"); } if (respuesta.status === 404) { throw new ErrorHttp(404, "Recurso no encontrado"); } if (respuesta.status >= 500) { throw new ErrorHttp(respuesta.status, "Error interno del servidor"); } if (!respuesta.ok) { throw new ErrorHttp(respuesta.status, respuesta.statusText); } return respuesta.json(); } async function probar() { try { const datos = await peticionSegura("https://api.ejemplo.com/recurso/999"); console.log(datos); } catch (err) { if (err instanceof ErrorHttp && err.status === 404) { console.log("El recurso no existe — mostrando estado vacío"); } else { console.error("Error inesperado:", err.message); } } }
class ErrorHttp extends Error { constructor( public readonly status: number, mensaje: string ) { super(mensaje); this.name = "ErrorHttp"; } } async function peticionSegura<T>( url: string, opciones: RequestInit = {} ): Promise<T> { let respuesta: Response; try { respuesta = await fetch(url, opciones); } catch (err) { const mensaje = err instanceof Error ? err.message : String(err); throw new Error(`Error de red: ${mensaje}`); } if (respuesta.status === 401) throw new ErrorHttp(401, "No autenticado"); if (respuesta.status === 403) throw new ErrorHttp(403, "Sin permisos"); if (respuesta.status === 404) throw new ErrorHttp(404, "Recurso no encontrado"); if (respuesta.status >= 500) throw new ErrorHttp(respuesta.status, "Error del servidor"); if (!respuesta.ok) throw new ErrorHttp(respuesta.status, respuesta.statusText); return respuesta.json() as Promise<T>; } async function probar(): Promise<void> { try { const datos = await peticionSegura<{ id: number }>( "https://api.ejemplo.com/recurso/999" ); console.log(datos); } catch (err) { if (err instanceof ErrorHttp && err.status === 404) { console.log("El recurso no existe — mostrando estado vacío"); } else if (err instanceof Error) { console.error("Error inesperado:", err.message); } } }

Tipado de respuestas en TypeScript

Las interfaces de API te dan autocompletado y detección de errores en tiempo de compilación:

// Tipos que mapean la estructura real de la API type PaginaRespuesta<T> = { data: T[]; total: number; pagina: number; porPagina: number; hayMas: boolean; }; type Producto = { id: number; nombre: string; precio: number; stock: number; categoriaId: number; }; // Cliente HTTP tipado y reutilizable class ClienteApi { constructor(private readonly baseUrl: string) {} async get<T>(ruta: string): Promise<T> { const respuesta = await fetch(`${this.baseUrl}${ruta}`); if (!respuesta.ok) { throw new Error(`GET ${ruta} falló con ${respuesta.status}`); } return respuesta.json() as Promise<T>; } async post<TBody, TRespuesta>(ruta: string, cuerpo: TBody): Promise<TRespuesta> { const respuesta = await fetch(`${this.baseUrl}${ruta}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(cuerpo), }); if (!respuesta.ok) { throw new Error(`POST ${ruta} falló con ${respuesta.status}`); } return respuesta.json() as Promise<TRespuesta>; } } const api = new ClienteApi("https://api.tienda.com"); async function cargarProductos(pagina: number): Promise<void> { // TypeScript infiere el tipo correcto gracias al genérico const resultado = await api.get<PaginaRespuesta<Producto>>( `/productos?pagina=${pagina}` ); // resultado.data es Producto[] — autocompletado completo resultado.data.forEach(p => { console.log(`${p.nombre}: $${p.precio}`); }); }
// Sin tipos — la misma lógica sin anotaciones class ClienteApi { constructor(baseUrl) { this.baseUrl = baseUrl; } async get(ruta) { const respuesta = await fetch(`${this.baseUrl}${ruta}`); if (!respuesta.ok) { throw new Error(`GET ${ruta} falló con ${respuesta.status}`); } return respuesta.json(); } async post(ruta, cuerpo) { const respuesta = await fetch(`${this.baseUrl}${ruta}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(cuerpo), }); if (!respuesta.ok) { throw new Error(`POST ${ruta} falló con ${respuesta.status}`); } return respuesta.json(); } } const api = new ClienteApi("https://api.tienda.com"); async function cargarProductos(pagina) { const resultado = await api.get(`/productos?pagina=${pagina}`); resultado.data.forEach(p => { console.log(`${p.nombre}: $${p.precio}`); }); }

Practica