Consumo de APIs web
Aprender a consumir APIs es esencial si quieres crear apps modernas y conectadas.
Hacer peticiones HTTP: tu primer contacto con una API
Una API web es, en esencia, un servicio que expone datos o funcionalidades a través de internet utilizando peticiones HTTP. Cuando tú, desde tu página web o tu aplicación, necesitas obtener o enviar información, realizas una petición a esa API. Esa interacción sigue siempre el mismo patrón: tú envías una solicitud y la API responde con datos, normalmente en JSON.
Para pedir esos datos desde JavaScript tienes dos formas muy comunes. La primera es Fetch API, que está integrada en todos los navegadores y no requiere instalación. Es un método moderno, basado en promesas, y sirve para hacer peticiones de forma sencilla. La segunda es Axios, una librería que debes instalar en tu proyecto, pero que añade utilidades muy cómodas, como manejar mejor los errores, cancelar peticiones, interceptarlas, o trabajar con datos más complejos.
Para entenderlo mejor, piensa en una API pública de libros. Tú quieres mostrar en tu web la lista de libros más vendidos. Necesitas “preguntar” a esa API y recuperar los datos.
Ejemplo usando Fetch API:
// Declaramos una función asíncrona porque usaremos 'await' dentro de ella.
// Esta función se encargará de pedir a la API una lista de libros.
async function cargarLibros() {
try {
// Realizamos la petición HTTP con fetch.
// Como fetch es una operación asíncrona, esperamos su resultado con 'await'.
// Hasta que no llegue la respuesta del servidor, el código no continúa.
const respuesta = await fetch("https://api.ejemplo.com/libros");
// Comprobamos si la respuesta fue correcta. La propiedad 'ok' es true
// solo cuando el servidor responde con un código entre 200 y 299.
if (!respuesta.ok) {
// Si la API respondió con un error (404, 500, etc.), lanzamos una excepción.
// Esto hace que el código salte directamente al bloque 'catch'.
throw new Error("Error al obtener los libros");
}
// Convertimos la respuesta a formato JSON.
// La API normalmente devuelve los datos como texto, y aquí los transformamos
// en un objeto de JavaScript para poder trabajar con ellos.
const datos = await respuesta.json();
// Mostramos los datos recibidos en la consola para revisar su contenido.
console.log("Libros:", datos);
} catch (error) {
// Si ocurre cualquier error (de red, de servidor, de JSON, etc.),
// entramos aquí. Mostramos el mensaje del error para entender qué pasó.
console.log("Error:", error.message);
}
}
// Llamamos a la función para ejecutar todo el proceso de carga de libros.
cargarLibros();
Ejemplo equivalente usando Axios:
// Importamos la librería Axios. Esto solo funciona si estás en un entorno
// donde puedas usar módulos (por ejemplo, un proyecto con Vite, Webpack o Node).
import axios from "axios";
// Declaramos una función asíncrona que se encargará de pedir los datos a la API.
async function cargarLibros() {
try {
// Realizamos una petición GET usando axios.
// Axios devuelve directamente un objeto ya procesado,
// por eso no necesitamos usar .json() como en fetch.
// El 'await' detiene la ejecución hasta recibir la respuesta.
const respuesta = await axios.get("https://api.ejemplo.com/libros");
// Mostramos en consola los datos. Con axios, la propiedad 'data'
// contiene el cuerpo de la respuesta ya parseado automáticamente.
console.log("Libros:", respuesta.data);
} catch (error) {
// Si ocurre un error, axios pasa un objeto de error más detallado
// que fetch. Usamos 'error.response' para saber si el servidor respondió
// con un código concreto como 404, 500, etc.
// Si no existe error.response (por ejemplo, si no hay conexión),
// mostramos el mensaje general del error.
console.log("Error:", error.response?.status || error.message);
}
}
// Llamamos a la función para ejecutar la carga de libros.
cargarLibros();
La diferencia principal es que Axios simplifica algunos pasos. Por ejemplo, no necesitas convertir la respuesta a JSON manualmente, ya que lo hace por ti.
Ejemplo real más concreto:
Imagina que usas la API pública de GitHub para mostrar información de usuarios.
Con Fetch:
const usuario = "torvalds";
fetch(`https://api.github.com/users/${usuario}`)
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.error(err));
Con Axios:
axios
.get(`https://api.github.com/users/${usuario}`)
.then(res => console.log(res.data))
.catch(err => console.error(err));
Ambas formas permiten lograr lo mismo, pero Axios incorpora características extra que hacen más cómodo trabajar con peticiones complejas.
Axios
Axios es, básicamente, un cliente HTTP escrito en JavaScript. Te sirve para hacer peticiones a APIs (GET, POST, PUT, DELETE, etc.) tanto desde el navegador como desde Node.js, pero con varias comodidades por encima de fetch.
1. Qué es Axios y qué hace “por dentro”
Cuando llamas a axios.get(...) o axios.post(...), estás haciendo tres cosas:
- Construyes una petición HTTP (URL, método, cabeceras, body…).
- Esa petición se envía al servidor (la API).
- Cuando el servidor responde, Axios:
- Te entrega la respuesta ya procesada en un objeto con esta forma típica:
data: el cuerpo de la respuesta (normalmente JSON ya parseado).status: código HTTP (200, 201, 404, 500…).headers: las cabeceras de respuesta.config: la configuración de la petición que se envió.
- Si la respuesta es un error HTTP (404, 500, etc.) o hay un problema de red, lanza un error y la promesa se rechaza → eso lo capturas en el
catch.
- Te entrega la respuesta ya procesada en un objeto con esta forma típica:
Axios está basado en promesas, así que lo usas con .then/.catch o con async/await.
2. Ejemplo real 1: Obtener datos de una API pública (GitHub)
Imagina que quieres mostrar en tu web información básica de un usuario de GitHub. GitHub tiene una API pública para esto.
import axios from "axios";
// Función asíncrona que pide datos de un usuario de GitHub
async function cargarUsuarioGitHub(username) {
try {
// Hacemos una petición GET a la API de GitHub.
// Fíjate que metemos el 'username' en la URL usando template literals.
const respuesta = await axios.get(`https://api.github.com/users/${username}`);
// Axios ya ha parseado la respuesta JSON y la mete en 'data'.
const usuario = respuesta.data;
// Mostramos algunos campos interesantes en consola.
console.log("Nombre:", usuario.name);
console.log("Login:", usuario.login);
console.log("Bio:", usuario.bio);
console.log("Número de repos públicos:", usuario.public_repos);
} catch (error) {
// Si hay error, puede ser por varios motivos:
// - El usuario no existe (404).
// - Problemas de red.
// - La API responde con otro código de error (403, 500, etc.)
if (error.response) {
// error.response existe cuando el servidor respondió con un código
// distinto de 2xx. Por ejemplo: 404, 500…
console.log("La API respondió con un error.");
console.log("Código de estado:", error.response.status);
console.log("Respuesta del servidor:", error.response.data);
} else if (error.request) {
// error.request existe cuando la petición se envió,
// pero no se recibió respuesta (problema de red, CORS, etc.)
console.log("No se recibió respuesta de la API.");
console.log("Petición enviada:", error.request);
} else {
// Cualquier otro tipo de error al preparar la petición.
console.log("Error al configurar la petición:", error.message);
}
}
}
// Llamamos a la función con un usuario de ejemplo.
cargarUsuarioGitHub("torvalds");
En este ejemplo ves varias cosas importantes:
axios.get(url)devuelve un objetorespuesta.respuesta.dataes el JSON ya convertido a objeto JS.- El manejo de errores es más rico:
error.response,error.request,error.message.
3. Ejemplo real 2: Enviar datos de un formulario (login / registro)
Ahora imagina algo muy típico en desarrollo web: un formulario de login que envía email y contraseña a una API.
import axios from "axios";
async function enviarLogin(email, password) {
try {
// Hacemos una petición POST a la ruta de login.
// En el segundo parámetro enviamos un objeto JS. Axios lo convierte a JSON
// automáticamente y añade la cabecera Content-Type: application/json
const respuesta = await axios.post("https://api.ejemplo.com/auth/login", {
email: email,
password: password,
});
// Si llegamos aquí es porque el servidor respondió con un código 2xx (200, 201…)
const datos = respuesta.data;
console.log("Login correcto.");
console.log("Token recibido:", datos.token);
console.log("Usuario:", datos.usuario);
// Aquí podrías guardar el token en localStorage, por ejemplo:
// localStorage.setItem("token", datos.token);
} catch (error) {
// En un login es muy habitual tener errores de credenciales.
if (error.response) {
// El servidor respondió con un código de error.
if (error.response.status === 401) {
console.log("Credenciales incorrectas. Revisa email y contraseña.");
} else {
console.log("Error en el servidor:", error.response.status);
console.log("Detalle:", error.response.data);
}
} else {
// Problemas de red u otro tipo de error.
console.log("No se pudo conectar con el servidor:", error.message);
}
}
}
// Ejemplo de uso (en la realidad vendría de un formulario)
enviarLogin("usuario@ejemplo.com", "123456");
Puntos clave:
- Con
axios.post(url, body)envías un objeto directamente; no necesitas hacerJSON.stringify. - Axios se encarga de poner las cabeceras adecuadas (
Content-Type). - El flujo de errores es muy natural: si el backend devuelve 401, cae en el
catch.
4. Ejemplo real 3: Instancia de Axios con baseURL y token de autenticación
En aplicaciones un poco más grandes, repites constantemente la misma base de URL y las mismas cabeceras (por ejemplo, el token JWT de autenticación). Para eso se usan instancias e interceptores.
Ejemplo: tienes un frontend con React o similar y toda tu API está bajo https://api.misitio.com/.
import axios from "axios";
// 1. Creamos una instancia de axios preconfigurada
const api = axios.create({
baseURL: "https://api.misitio.com", // todas las rutas partirán de aquí
timeout: 5000, // tiempo máximo de espera en milisegundos
});
// 2. Interceptor de petición: se ejecuta ANTES de enviar cada petición.
// Lo usamos, por ejemplo, para añadir el token de autenticación.
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem("token");
if (token) {
// Añadimos la cabecera Authorization solo si existe token.
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
// Si hay un error al configurar la petición, lo rechazamos.
return Promise.reject(error);
}
);
// 3. Interceptor de respuesta: se ejecuta CUANDO llega la respuesta.
// Aquí puedes centralizar el manejo de errores (401 global, redirecciones, etc.)
api.interceptors.response.use(
(response) => {
// Si todo va bien, devolvemos la respuesta tal cual.
return response;
},
(error) => {
if (error.response && error.response.status === 401) {
console.log("No autorizado. Quizá el token ha caducado.");
// Aquí podrías hacer un logout global, redirigir a login, etc.
}
return Promise.reject(error);
}
);
// 4. Funciones de “servicio” que usan la instancia api:
// Obtener lista de libros
export async function getLibros() {
const respuesta = await api.get("/libros"); // en realidad llama a https://api.misitio.com/libros
return respuesta.data; // devolvemos solo los datos útiles
}
// Crear un libro nuevo
export async function crearLibro(libro) {
const respuesta = await api.post("/libros", libro);
return respuesta.data;
}
// Ejemplo de uso en cualquier parte de tu app
async function ejemploUso() {
try {
const libros = await getLibros();
console.log("Libros en la base de datos:", libros);
} catch (error) {
console.log("Error al cargar libros:", error.message);
}
}
ejemploUso();
Aquí ves una forma “profesional” de usar Axios:
- Centralizas la
baseURL, eltimeout, el token, el manejo de errores, etc., en un solo lugar. - El resto del código de tu app se limita a llamar a
getLibros(),crearLibro(), etc., sin preocuparse por detalles de red.
Manejo de estados de carga y error en interfaces
Cuando haces una petición a una API, no basta con obtener los datos. La interfaz debe comunicar al usuario qué está pasando. Si no lo haces, el usuario:
- No sabe si algo está cargando,
- No sabe si el resultado se procesó,
- No entiende si ha ocurrido un error.
Por eso se usan tres estados fundamentales:
- Cargando,
- Éxito,
- Error.
Ejemplo real: cargar lista de usuarios con HTML + fetch
Imagina que tienes esta interfaz mínima:
<div id="estado"></div>
<ul id="lista"></ul>
<button id="recargar">Cargar usuarios</button>
Queremos:
- Mostrar un mensaje de “Cargando…” mientras llega la respuesta.
- Mostrar los usuarios cuando todo vaya bien.
- Mostrar un mensaje claro si ocurre un error.
Ejemplo completo con fetch y tres estados
const estado = document.getElementById("estado");
const lista = document.getElementById("lista");
const boton = document.getElementById("recargar");
boton.addEventListener("click", cargarUsuarios);
async function cargarUsuarios() {
try {
// Estado 1: Cargando.
// Antes de hacer la petición indicamos en la interfaz que algo está ocurriendo.
estado.textContent = "Cargando usuarios...";
lista.innerHTML = "";
// Llamada a la API con fetch.
const respuesta = await fetch("https://jsonplaceholder.typicode.com/users");
// Si la API responde con un estado que no es 200-299, lo tratamos como error.
if (!respuesta.ok) {
throw new Error("La API respondió con un código incorrecto");
}
// Estado 2: Éxito.
// Convertimos la respuesta JSON.
const datos = await respuesta.json();
// Mostramos los datos en la interfaz.
datos.forEach(usuario => {
const li = document.createElement("li");
li.textContent = `${usuario.name} (${usuario.email})`;
lista.appendChild(li);
});
// Actualizamos el mensaje de estado.
estado.textContent = "Usuarios cargados correctamente.";
} catch (error) {
// Estado 3: Error.
// Si la petición falla por cualquier motivo (red, servidor, código no OK, JSON inválido)
// mostramos un mensaje claro para el usuario.
estado.textContent = "Error al cargar usuarios. Intente de nuevo más tarde.";
// También podemos mostrar detalles para el desarrollador en consola.
console.log("Error interno:", error.message);
}
}
Explicación clara de cada estado usando fetch
Estado de carga
Justo antes de llamar a fetch, debes mostrar algo como “Cargando…”.
Motivo práctico:
El fetch tarda un tiempo (corto o largo según la red o el servidor). Si el usuario pulsa un botón y no pasa nada visualmente, piensa que está roto o pulsa varias veces. Indicar “Cargando…” evita confusiones y duplicaciones.
En el ejemplo:
estado.textContent = "Cargando usuarios...";
lista.innerHTML = "";
La interfaz comunica que la aplicación está trabajando.
Estado de éxito
Significa que:
- Fetch no falló,
- La API devolvió un código correcto (
response.ok), - Los datos se procesaron y se pueden mostrar.
En ese momento:
- Se actualiza la interfaz con los nuevos datos,
- Se informa al usuario de que ya está disponible.
Ejemplo:
const datos = await respuesta.json();
datos.forEach(usuario => { ... });
estado.textContent = "Usuarios cargados correctamente.";
Sin este estado, el usuario no sabría si los datos son nuevos, viejos o incompletos.
Estado de error
Aquí entran muchos casos reales:
- Error de red.
- El servidor devuelve 500.
- La URL está mal.
- fetch devuelve un 404.
- La respuesta no es JSON válido.
- El backend rechaza la operación por permisos.
Si no informas del error:
- El usuario cree que el sistema “no hace nada”,
- No entiende qué debe hacer a continuación.
Ejemplo:
estado.textContent = "Error al cargar usuarios. Intente de nuevo más tarde.";
console.log("Error interno:", error.message);
La interfaz sigue operativa y el usuario sabe qué ocurrió.
Mini proyecto completo
- Backend con Node.js puro (ES modules) que expone
/api/librosy puede simular errores. - Frontend con Bootstrap 5.3.x por CDN y Fetch API, mostrando estados de carga, éxito y error.
1. Backend: Node.js puro con ES modules
Archivo: server.mjs
// server.mjs
// Backend mínimo con Node.js puro (sin Express) usando ES modules.
// Expone un endpoint GET /api/libros que devuelve una lista de libros en JSON.
// Si se llama con ?error=1 simula un error 500 para probar el estado de error en la UI.
import http from "node:http";
import { URL } from "node:url";
// Datos de ejemplo (en un proyecto real vendrían de una base de datos)
const libros = [
{ id: 1, titulo: "El nombre del viento", autor: "Patrick Rothfuss", anio: 2007 },
{ id: 2, titulo: "Clean Code", autor: "Robert C. Martin", anio: 2008 },
{ id: 3, titulo: "JavaScript: The Good Parts", autor: "Douglas Crockford", anio: 2008 },
];
// Función auxiliar para enviar respuestas JSON
function enviarJSON(res, statusCode, data) {
res.writeHead(statusCode, {
"Content-Type": "application/json; charset=utf-8",
// Cabecera CORS para permitir llamadas desde un index.html abierto en el navegador
"Access-Control-Allow-Origin": "*",
});
res.end(JSON.stringify(data));
}
const server = http.createServer((req, res) => {
// Construimos la URL completa para poder leer query params con la clase URL
const url = new URL(req.url, `http://${req.headers.host}`);
// Ruta: GET /api/libros
if (req.method === "GET" && url.pathname === "/api/libros") {
// Si viene ?error=1, simulamos un fallo de servidor
const forzarError = url.searchParams.get("error") === "1";
if (forzarError) {
// Estado de error simulado: 500 Internal Server Error
return enviarJSON(res, 500, {
mensaje: "Error simulado en el servidor al obtener los libros.",
});
}
// Estado de éxito: devolvemos la lista de libros con código 200
return enviarJSON(res, 200, libros);
}
// Cualquier otra ruta: 404
enviarJSON(res, 404, { mensaje: "Ruta no encontrada" });
});
// Ponemos el servidor a escuchar en el puerto 3000
const PORT = 3000;
server.listen(PORT, () => {
console.log(`Servidor HTTP escuchando en http://localhost:${PORT}`);
});
Para ejecutarlo (en Windows o donde sea):
node server.mjs
Con eso tienes el backend listo en http://localhost:3000/api/libros.
2. Frontend: Bootstrap 5 + Fetch API con estados (cargando, éxito, error)
Archivo: index.html
(Lo puedes abrir directamente en el navegador; como el servidor devuelve CORS *, no tendrás problemas.)
Este archivo incluye Bootstrap 5.3.x por CDN y muestra una interfaz sencilla con:
- Botón “Cargar libros” → modo normal.
- Botón “Forzar error” → llama a la API con
?error=1para probar el estado de error. - Spinner de carga (estado "cargando").
- Lista de libros (estado "éxito").
- Alertas Bootstrap para mensajes de éxito o error.
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8" />
<title>Ejemplo Fetch + Bootstrap: estados de carga, éxito y error</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Bootstrap 5.3.x CSS desde CDN -->
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-7xN3H1z6OZnsdcvumyW4rNQwdqT6nkb5chBo2N4dFv6R6Lzvuz8eGgM1pXkz3+7M"
crossorigin="anonymous"
/>
<style>
/* Solo un pequeño ajuste para el aspecto general */
body {
padding-top: 3rem;
background-color: #f8f9fa;
}
</style>
</head>
<body>
<div class="container">
<h1 class="mb-4">Libros (Fetch API + Bootstrap)</h1>
<!-- Zona de alertas (éxito / error) -->
<div id="alertPlaceholder"></div>
<!-- Card principal con controles y lista -->
<div class="card shadow-sm">
<div class="card-body">
<h5 class="card-title">Gestión de estados con Fetch</h5>
<p class="card-text">
Este ejemplo muestra cómo manejar los estados de <strong>carga</strong>,
<strong>éxito</strong> y <strong>error</strong> al hacer peticiones con Fetch
a una API hecha con Node.js.
</p>
<!-- Botones de acción -->
<div class="d-flex gap-2 mb-3">
<button id="btnCargar" class="btn btn-primary">
Cargar libros
</button>
<button id="btnForzarError" class="btn btn-outline-danger">
Forzar error del servidor
</button>
</div>
<!-- Estado de carga: spinner + texto -->
<div
id="loading"
class="d-none d-flex align-items-center mb-3"
aria-live="polite"
>
<div class="spinner-border" role="status" aria-hidden="true"></div>
<span class="ms-2">Cargando datos, por favor espera...</span>
</div>
<!-- Lista de libros -->
<ul id="listaLibros" class="list-group"></ul>
<!-- Estado "sin datos" -->
<p id="sinDatos" class="text-muted mt-3 d-none">
No hay libros cargados todavía.
</p>
</div>
</div>
</div>
<!-- Bootstrap JS (opcional para este ejemplo, pero útil si usas componentes JS) -->
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"
integrity="sha384-ZQ23yX5tJM1OTwcG8M1mUYBv0q5sYbNk+eLYZFdMGz9Cjf3R+R0p3qgztE+f4JvC"
crossorigin="anonymous"
></script>
<script>
// Referencias a elementos del DOM
const btnCargar = document.getElementById("btnCargar");
const btnForzarError = document.getElementById("btnForzarError");
const loading = document.getElementById("loading");
const listaLibros = document.getElementById("listaLibros");
const sinDatos = document.getElementById("sinDatos");
const alertPlaceholder = document.getElementById("alertPlaceholder");
const API_BASE = "http://localhost:3000/api/libros";
// Función para mostrar una alerta de Bootstrap (éxito, error, etc.)
// type: "success", "danger", "warning", "info"...
function showAlert(message, type = "info") {
alertPlaceholder.innerHTML = `
`;
}
// Función para mostrar u ocultar el spinner de carga
function setLoading(isLoading) {
if (isLoading) {
loading.classList.remove("d-none");
// Desactivamos los botones mientras carga para evitar peticiones duplicadas
btnCargar.disabled = true;
btnForzarError.disabled = true;
} else {
loading.classList.add("d-none");
btnCargar.disabled = false;
btnForzarError.disabled = false;
}
}
// Función para limpiar la lista y el mensaje "sin datos"
function resetList() {
listaLibros.innerHTML = "";
sinDatos.classList.add("d-none");
}
// Renderiza la lista de libros en la interfaz (estado de éxito)
function renderLibros(libros) {
resetList();
if (!Array.isArray(libros) || libros.length === 0) {
// Si el array está vacío, mostramos el estado "sin datos"
sinDatos.textContent = "No se han recibido libros desde la API.";
sinDatos.classList.remove("d-none");
return;
}
libros.forEach((libro) => {
const li = document.createElement("li");
li.className =
"list-group-item d-flex justify-content-between align-items-center";
li.innerHTML = `
`;
listaLibros.appendChild(li);
});
}
// Función principal que gestiona el ciclo de estados:
// 1. Cargando
// 2. Éxito (si todo va bien)
// 3. Error (si algo falla)
async function cargarLibros({ forzarError = false } = {}) {
// Borramos alertas anteriores y lista
alertPlaceholder.innerHTML = "";
resetList();
// Estado de carga: activamos el spinner y deshabilitamos botones
setLoading(true);
try {
// Construimos la URL. Si forzamos error, añadimos ?error=1
const url = forzarError ? `${API_BASE}?error=1` : API_BASE;
const response = await fetch(url);
// Si la respuesta no está en el rango 200-299, la tratamos como error
if (!response.ok) {
// Aquí ya podamos lanzar un error con info útil
throw new Error(`Error HTTP ${response.status}`);
}
const data = await response.json();
// Estado de éxito: mostramos los datos
renderLibros(data);
showAlert("Libros cargados correctamente.", "success");
} catch (err) {
// Estado de error: mostramos mensaje amigable en la UI
console.error("Error al cargar libros:", err);
showAlert(
"No se pudieron cargar los libros. Inténtalo de nuevo más tarde.",
"danger"
);
// Opcional: mostrar también texto en la zona "sin datos"
sinDatos.textContent = "Ha ocurrido un error al obtener los datos.";
sinDatos.classList.remove("d-none");
} finally {
// Siempre se ejecuta: tanto si fue éxito como si fue error
// Apagamos el estado de carga
setLoading(false);
}
}
// Eventos de los botones
btnCargar.addEventListener("click", () => {
cargarLibros({ forzarError: false });
});
btnForzarError.addEventListener("click", () => {
cargarLibros({ forzarError: true });
});
// Si quieres, puedes cargar la lista automáticamente al abrir la página:
// cargarLibros();
</script>
</body>
</html>
Manejar errores y respuestas de forma elegante con Axios
Con Axios puedes manejar respuestas y errores de forma mucho más elegante que con fetch, sobre todo si sigues una idea clave:
Deja que Axios se encargue del “caos” de red y tú trabaja siempre con una capa sencilla y consistente: datos limpios y errores normalizados.
1. Cómo maneja Axios las respuestas y los errores
1.1. Qué devuelve Axios cuando todo va bien
Cuando una petición tiene éxito (código HTTP 2xx), Axios resuelve la promesa con un objeto de respuesta que, por defecto, tiene esta forma:
{
data: ..., // cuerpo de la respuesta (normalmente JSON ya parseado)
status: 200, // código HTTP
statusText: "OK",
headers: {...},
config: {...}, // configuración de la petición que se envió
request: ... // objeto de bajo nivel (XMLHttpRequest, http.ClientRequest, etc.)
}
Lo más importante en el día a día:
response.data: los datos que realmente quieres.response.status: el código HTTP (200, 201, 204…).
Muchas veces, en tu código de aplicación solo te interesa data, así que es muy común usar un interceptor para que Axios te devuelva directamente data en lugar del objeto completo.
1.2. Qué pasa cuando algo sale mal: el objeto de error
Cuando la petición falla, Axios rechaza la promesa con un objeto error. Ese objeto suele tener estas propiedades:
error.response: existe si el servidor respondió con un código HTTP de error (400, 401, 404, 500…).error.response.statuserror.response.data
error.request: existe si la petición se envió pero no se recibió respuesta (problema de red, CORS, servidor caído, etc.).error.message: texto del error.error.config: configuración de la petición que generó el error.
Esto te permite distinguir:
-
Error de servidor / negocio (respuesta con código 4xx o 5xx)
Hay
error.response.Ejemplo: credenciales incorrectas, recurso no encontrado, validación de datos.
-
Error de red / conexión (no hay respuesta)
Hay
error.requestpero noerror.response.Ejemplo: el usuario está sin internet.
-
Error de configuración / código
No hay ni
responsenirequest; solomessage.Ejemplo: URL mal formada antes de enviar la petición.
1.3. Manejo “elegante”: patrones habituales
Un manejo elegante de errores y respuestas con Axios suele incluir:
-
Instancia centralizada de Axios
Crear algo como
api = axios.create({ baseURL, timeout, headers… })para no repetir lógica por todo el código. -
Interceptors de respuesta
- En el
successdel interceptor de respuesta, puedes devolverresponse.datadirectamente. - En el
errordel interceptor, puedes transformar el error de Axios en un objeto más simple y coherente para toda la app (por ejemplo,{ message, status, isNetworkError, details }).
- En el
-
Mensaje de error para el usuario vs. detalles para el desarrollador
- Mensajes simples y claros para la UI.
- Detalles, stack y códigos en consola o logs internos.
-
Códigos de error significativos en el backend
Si controlas la API, puedes hacer que el backend devuelva algo tipo
{ code: "INVALID_CREDENTIALS", message: "Usuario o contraseña incorrectos" }, y en el frontend traducircodea mensajes de interfaz.
La idea: todo tu código de frontend trabaja con un contrato estable de error, aunque la red y el backend sean caóticos.
2. Ejemplo completo: instancia de Axios con manejo elegante de errores
Te preparo un ejemplo muy típico de uso “profesional”:
- Tenemos una API (falsa o real) en
https://api.ejemplo.com. - Creamos una instancia
apide Axios. - Configuramos interceptores para:
- que las respuestas exitosas devuelvan solo
data; - que los errores se normalicen a un objeto sencillo.
- que las respuestas exitosas devuelvan solo
- Usamos esa instancia en una función que carga usuarios y maneja errores de forma clara.
Imagina estos archivos en un proyecto frontend moderno (Vite, Webpack, etc.), usando ES modules.
2.1. Archivo apiClient.js: instancia Axios + interceptores
// apiClient.js
// Instancia central de Axios con manejo elegante de respuestas y errores.
import axios from "axios";
// 1. Creamos la instancia base de Axios
const api = axios.create({
baseURL: "https://jsonplaceholder.typicode.com", // API de ejemplo
timeout: 5000, // 5 segundos
});
// 2. Interceptor de respuesta (caso de éxito)
// En lugar de devolver el objeto completo de axios, devolvemos solo response.data
api.interceptors.response.use(
(response) => {
// Aquí puedes transformar datos si quieres (por ejemplo, mapear campos)
return response.data;
},
(error) => {
// 3. Interceptor de respuesta (caso de error)
// Aquí normalizamos el error a un formato propio para la app, más simple.
let normalizedError = {
message: "Ha ocurrido un error desconocido.",
status: null,
isNetworkError: false,
details: null,
};
if (error.response) {
// El servidor respondió con un código fuera del rango 2xx.
normalizedError.status = error.response.status;
// Intentamos usar el mensaje que venga del backend, si existe.
// Si no, ponemos un texto genérico según el tipo de error.
const backendMessage =
(error.response.data && error.response.data.message) || null;
if (backendMessage) {
normalizedError.message = backendMessage;
} else if (error.response.status >= 500) {
normalizedError.message =
"El servidor ha encontrado un problema. Inténtalo más tarde.";
} else if (error.response.status === 404) {
normalizedError.message = "Recurso no encontrado.";
} else if (error.response.status === 400) {
normalizedError.message = "La petición no es válida.";
} else if (error.response.status === 401 || error.response.status === 403) {
normalizedError.message =
"No tienes permisos para realizar esta acción o la sesión ha caducado.";
}
normalizedError.details = error.response.data || null;
} else if (error.request) {
// La petición se hizo, pero no se recibió respuesta.
normalizedError.isNetworkError = true;
normalizedError.message =
"No se ha podido contactar con el servidor. Comprueba tu conexión.";
normalizedError.details = null;
} else {
// Ocurrió un error al configurar la petición.
normalizedError.message =
error.message || "Error al preparar la petición. Revisa la configuración.";
}
// Opcional: log para desarrollador
console.error("Axios error original:", error);
// Rechazamos la promesa con el error normalizado, no con el error nativo de Axios
return Promise.reject(normalizedError);
}
);
// Exportamos la instancia ya configurada
export default api;
Punto importante:
A partir de aquí, cualquier api.get(...), api.post(...), etc.:
- En éxito → te devuelve directamente
data, noresponse. - En error → te lanza un objeto tipo:
{
message: "Texto amigable para la UI",
status: 404,
isNetworkError: false,
details: {...} // opcional
}
2.2. Archivo usuariosService.js: funciones de “servicio” usando apiClient
// usuariosService.js
// Capa de servicio que encapsula llamadas a la API relacionadas con usuarios.
import api from "./apiClient.js";
// Obtiene la lista de usuarios
export async function obtenerUsuarios() {
// Gracias al interceptor, esto devolverá directamente un array de usuarios
// (lo que haya en response.data de la API)
return await api.get("/users");
}
// Obtiene un usuario por id
export async function obtenerUsuarioPorId(id) {
return await api.get(`/users/${id}`);
}
Fíjate que aquí ya no hay lógica de errores ni manejo de response.data:
eso está encapsulado en apiClient.js.
2.3. Archivo main.js: ejemplo de uso elegante con UI básica
Imagina ahora un HTML muy simple:
<div id="estado"></div>
<ul id="lista-usuarios"></ul>
<button id="btn-cargar">Cargar usuarios</button>
Y este JS:
// main.js
// Uso de usuariosService + manejo elegante de errores normalizados.
import { obtenerUsuarios } from "./usuariosService.js";
const estado = document.getElementById("estado");
const lista = document.getElementById("lista-usuarios");
const btnCargar = document.getElementById("btn-cargar");
btnCargar.addEventListener("click", cargarUsuarios);
async function cargarUsuarios() {
// Estado: limpiamos UI previa
estado.textContent = "";
lista.innerHTML = "";
btnCargar.disabled = true;
estado.textContent = "Cargando usuarios...";
try {
const usuarios = await obtenerUsuarios();
if (!Array.isArray(usuarios) || usuarios.length === 0) {
estado.textContent = "No se han encontrado usuarios.";
return;
}
// Éxito: pintamos la lista
usuarios.forEach((usuario) => {
const li = document.createElement("li");
li.textContent = `${usuario.name} (${usuario.email})`;
lista.appendChild(li);
});
estado.textContent = "Usuarios cargados correctamente.";
} catch (error) {
// Aquí usamos el error normalizado por el interceptor
console.log("Error normalizado:", error);
// Mensaje principal para la UI
estado.textContent = error.message || "Ha ocurrido un error al cargar usuarios.";
// Si queremos, podemos reaccionar según el tipo:
if (error.isNetworkError) {
// Podrías mostrar un mensaje diferente o permitir reintentar más tarde
console.log("Sugerencia: informar al usuario de revisar su conexión.");
}
if (error.status === 401 || error.status === 403) {
console.log("Sugerencia: redirigir a login o pedir reautenticación.");
}
} finally {
// Siempre se ejecuta: éxito o error
btnCargar.disabled = false;
}
}
Observa lo que hemos conseguido:
-
El código de negocio (
main.js) no se preocupa de cómo Axios estructura el error. -
Solo conoce un contrato simple:
error.message,error.status,error.isNetworkError. -
El resto de la complejidad (distinción entre
error.response,error.request, etc.) está escondida enapiClient.js.
Eso es, precisamente, lo que suele llamarse manejar errores de forma “elegante”:
- Centralizado.
- Consistente.
- Fácil de reutilizar en todo el proyecto.
Ejemplo completo: Bootstrap 5 + Axios + estados (carga, éxito, error)
Guarda esto como index.html, abre tu backend o usa la API pública que ves en el código, y después abre el HTML en el navegador.
En este ejemplo usaremos la API pública de jsonplaceholder.typicode.com para obtener usuarios, y simularemos un error llamando a una ruta incorrecta.
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8" />
<title>Axios + Bootstrap: manejo elegante de errores y estados</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Bootstrap 5.3.x CSS -->
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css"
rel="stylesheet"
/>
<style>
body {
padding-top: 3rem;
background-color: #f8f9fa;
}
</style>
</head>
<body>
<div class="container">
<h1 class="mb-4">Usuarios (Axios + Bootstrap)</h1>
<!-- Zona de alertas -->
<div id="alertPlaceholder"></div>
<div class="card shadow-sm">
<div class="card-body">
<h5 class="card-title">Manejo elegante de respuestas y errores</h5>
<p class="card-text">
Este ejemplo usa una instancia de Axios con interceptores para:
<strong>normalizar errores</strong>, simplificar respuestas y mostrar
estados de <strong>carga</strong>, <strong>éxito</strong> y
<strong>error</strong> en la interfaz usando Bootstrap.
</p>
<!-- Botones de acción -->
<div class="d-flex gap-2 mb-3">
<button id="btnCargar" class="btn btn-primary">
Cargar usuarios
</button>
<button id="btnForzarError" class="btn btn-outline-danger">
Simular error
</button>
</div>
<!-- Estado de carga -->
<div
id="loading"
class="d-none d-flex align-items-center mb-3"
aria-live="polite"
>
<div class="spinner-border" role="status" aria-hidden="true"></div>
<span class="ms-2">Cargando datos, por favor espera...</span>
</div>
<!-- Lista de usuarios -->
<ul id="listaUsuarios" class="list-group"></ul>
<!-- Estado "sin datos" -->
<p id="sinDatos" class="text-muted mt-3 d-none">
No hay usuarios cargados todavía.
</p>
</div>
</div>
</div>
<!-- Bootstrap JS (bundle, por si quisieras usar componentes JS) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
<!-- Axios desde CDN: expone 'axios' en el global -->
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
// Referencias al DOM
const btnCargar = document.getElementById("btnCargar");
const btnForzarError = document.getElementById("btnForzarError");
const loading = document.getElementById("loading");
const listaUsuarios = document.getElementById("listaUsuarios");
const sinDatos = document.getElementById("sinDatos");
const alertPlaceholder = document.getElementById("alertPlaceholder");
// 1. Creamos una instancia de Axios con baseURL y timeout
const api = axios.create({
baseURL: "https://jsonplaceholder.typicode.com",
timeout: 5000,
});
// 2. Interceptor de respuesta (caso de éxito)
// Devolvemos directamente 'data' para simplificar el código de la app.
api.interceptors.response.use(
(response) => {
return response.data;
},
(error) => {
// 3. Interceptor de respuesta (caso de error)
// Normalizamos el error a un formato sencillo.
const normalizedError = {
message: "Ha ocurrido un error desconocido.",
status: null,
isNetworkError: false,
details: null,
};
if (error.response) {
// El servidor respondió con un código fuera de 2xx.
normalizedError.status = error.response.status;
const backendMessage =
(error.response.data && error.response.data.message) || null;
if (backendMessage) {
normalizedError.message = backendMessage;
} else if (error.response.status >= 500) {
normalizedError.message =
"El servidor ha encontrado un problema. Inténtalo más tarde.";
} else if (error.response.status === 404) {
normalizedError.message = "Recurso no encontrado en la API.";
} else if (error.response.status === 400) {
normalizedError.message = "La petición enviada no es válida.";
} else if (
error.response.status === 401 ||
error.response.status === 403
) {
normalizedError.message =
"No tienes permisos o la sesión ha caducado.";
}
normalizedError.details = error.response.data || null;
} else if (error.request) {
// La petición se hizo, pero no hubo respuesta
normalizedError.isNetworkError = true;
normalizedError.message =
"No se ha podido contactar con el servidor. Revisa tu conexión a internet.";
} else {
// Error al configurar la petición antes de enviarla
normalizedError.message =
error.message ||
"Error al preparar la petición. Revisa la configuración.";
}
console.error("Error original de Axios:", error);
// Rechazamos con el error normalizado
return Promise.reject(normalizedError);
}
);
// Función para mostrar alertas Bootstrap
function showAlert(message, type = "info") {
alertPlaceholder.innerHTML = `
`;
}
// Mostrar / ocultar spinner de carga
function setLoading(isLoading) {
if (isLoading) {
loading.classList.remove("d-none");
btnCargar.disabled = true;
btnForzarError.disabled = true;
} else {
loading.classList.add("d-none");
btnCargar.disabled = false;
btnForzarError.disabled = false;
}
}
// Limpiar lista y mensaje "sin datos"
function resetList() {
listaUsuarios.innerHTML = "";
sinDatos.classList.add("d-none");
}
// Pintar usuarios en la interfaz
function renderUsuarios(usuarios) {
resetList();
if (!Array.isArray(usuarios) || usuarios.length === 0) {
sinDatos.textContent = "No se han recibido usuarios desde la API.";
sinDatos.classList.remove("d-none");
return;
}
usuarios.forEach((usuario) => {
const li = document.createElement("li");
li.className =
"list-group-item d-flex justify-content-between align-items-center";
li.innerHTML = `
`;
listaUsuarios.appendChild(li);
});
}
// Función principal para cargar usuarios
async function cargarUsuarios({ simularError = false } = {}) {
alertPlaceholder.innerHTML = "";
resetList();
setLoading(true);
try {
let data;
if (simularError) {
// Llamamos a una ruta incorrecta para provocar un 404
data = await api.get("/usuarios-que-no-existen");
} else {
// Ruta correcta de la API pública
data = await api.get("/users");
}
// Éxito: data ya es response.data gracias al interceptor
renderUsuarios(data);
showAlert("Usuarios cargados correctamente.", "success");
} catch (error) {
// Aquí recibimos el error normalizado por el interceptor
console.log("Error normalizado:", error);
showAlert(error.message, "danger");
sinDatos.textContent = "Ha ocurrido un error al obtener los datos.";
sinDatos.classList.remove("d-none");
if (error.isNetworkError) {
console.log(
"Sugerencia: informar al usuario de que revise su conexión."
);
}
if (error.status === 401 || error.status === 403) {
console.log(
"Sugerencia: redirigir a login o volver a pedir autenticación."
);
}
} finally {
setLoading(false);
}
}
// Eventos
btnCargar.addEventListener("click", () =>
cargarUsuarios({ simularError: false })
);
btnForzarError.addEventListener("click", () =>
cargarUsuarios({ simularError: true })
);
// Si quieres, puedes cargar automáticamente al entrar:
// cargarUsuarios();
</script>
</body>
</html>
Qué es este ejemplo
-
Axios como cliente HTTP serio
Una instancia
apiconbaseURLytimeout. -
Interceptors de respuesta
- En éxito: devuelves solo
response.data, simplificando el resto del código. - En error: conviertes el error “feo” de Axios en un objeto limpio con:
messagestatusisNetworkErrordetails
- En éxito: devuelves solo
-
Manejo de estados de interfaz con Bootstrap
- Cargando: spinner + botones desactivados.
- Éxito: alert de éxito + lista renderizada.
- Error: alert de error + texto “sin datos” y lógica condicional según tipo de error.
-
Separación de responsabilidades dentro del mismo archivo
Aunque todo está en un
index.htmlpara la demo, se ve claramente qué parte se podría extraer a módulos:api, servicios, UI, etc.
Qué es una API paginada y dónde aparece
Cuando una API devuelve muchísimos elementos (usuarios, productos, posts, pedidos, etc.), no tiene sentido devolverlos todos de golpe:
- Es lento,
- Consume mucho ancho de banda,
- Puede bloquear el navegador/dispositivo,
- Y a menudo el usuario solo necesita ver una parte (la “página” actual).
Por eso muchas APIs devuelven los datos paginados: en “trozos” o “páginas”, por ejemplo 10, 20 o 50 elementos cada vez.
Ejemplos reales donde se usa:
- Listado de productos en una tienda online:
GET /api/productos?page=3&pageSize=20 - Listado de usuarios en un panel de administración:
GET /api/usuarios?offset=40&limit=20 - Resultados de búsqueda:
GET /api/busqueda?q=javascript&page=2 - Historial de pedidos:
GET /api/pedidos?cursor=abc123(paginación por cursor)
Formas típicas de paginación en APIs:
-
page + pageSize / per_page
Ej:
GET /api/libros?page=2&pageSize=10Devuelve la segunda página, 10 libros por página.
-
offset + limit
Ej:
GET /api/libros?offset=20&limit=10Empieza en el elemento 20, trae 10.
-
cursor / nextPageToken (más “pro”)
Ej:
GET /api/libros?cursor=eyJpZCI6MTIzfQ==El backend devuelve un token para pedir la siguiente página.
Todas buscan lo mismo: no traigas todo, trae solo un trozo y dame información para pedir el siguiente.
Cómo administrar eficientemente una API paginada (en frontend)
La idea elegante es:
- Tener una capa que sabe hablar con la API (construye URL, añade parámetros, maneja errores).
- Tener una capa que entiende de “páginas” (estado actual, siguiente, anterior, cuántas hay).
- Tener una capa de UI que solo se preocupa de mostrar datos y reaccionar a eventos (click en “Siguiente”, etc.).
Te voy a montar un ejemplo modular con Fetch API, asumiendo que tu backend expone algo así:
GET /api/libros?page=1&pageSize=5
Y devuelve JSON con esta forma:
{
"items": [
{ "id": 1, "titulo": "Libro 1", "autor": "Autor 1" },
{ "id": 2, "titulo": "Libro 2", "autor": "Autor 2" }
],
"page": 1,
"pageSize": 5,
"totalPages": 10,
"totalItems": 50
}
Vamos con los archivos.
apiClient.js
Responsabilidad: hablar con la API (baseURL, query params, manejo básico de errores).
// apiClient.js
// Pequeño cliente HTTP basado en Fetch, centralizado.
// Aquí se define la baseURL y una función GET genérica con soporte de query params.
const BASE_URL = "http://localhost:3000/api"; // ajusta a tu backend real
/**
* Construye una URL con parámetros de consulta (query string).
* @param {string} path - Ruta relativa, por ejemplo "/libros"
* @param {object} [params={}] - Objeto de parámetros { page: 1, pageSize: 10 }
* @returns {string} URL completa
*/
function buildUrl(path, params = {}) {
const url = new URL(path, BASE_URL);
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
url.searchParams.set(key, String(value));
}
});
return url.toString();
}
/**
* Hace una petición GET y devuelve JSON ya parseado.
* Lanza un error si la respuesta HTTP no es "ok".
* @param {string} path - Ruta (ej: "/libros")
* @param {object} [params] - Query params (ej: { page: 1, pageSize: 5 })
*/
export async function get(path, params) {
const url = buildUrl(path, params);
const response = await fetch(url);
if (!response.ok) {
// Lanzamos un error con algo de información útil
const body = await response.text().catch(() => "");
const message = `Error HTTP ${response.status} al llamar a ${url}`;
const error = new Error(message);
error.status = response.status;
error.body = body;
throw error;
}
return response.json();
}
librosService.js
Responsabilidad: lógica de dominio de “libros”.
No sabe nada de DOM ni UI, solo de “obtener página de libros”.
// librosService.js
// Capa de servicio específica para "libros".
// Usa apiClient para pedir páginas de libros a la API.
import { get } from "./apiClient.js";
/**
* Obtiene una página de libros desde la API.
* @param {number} page - Número de página (1, 2, 3…)
* @param {number} pageSize - Tamaño de página (cuántos libros por página)
* @returns {Promise<{items: any[], page: number, pageSize: number, totalPages: number, totalItems: number}>}
*/
export async function fetchLibrosPage(page = 1, pageSize = 5) {
// Llamamos a GET /libros?page=...&pageSize=...
const data = await get("/libros", { page, pageSize });
// Aquí podrías validar el shape de 'data' o transformarlo
return {
items: data.items ?? [],
page: data.page ?? page,
pageSize: data.pageSize ?? pageSize,
totalPages: data.totalPages ?? 1,
totalItems: data.totalItems ?? data.items?.length ?? 0,
};
}
paginationManager.js
Responsabilidad: gestionar el estado de paginación (página actual, total, si hay siguiente/anterior, etc.).
// paginationManager.js
// Clase simple para gestionar el estado de una paginación.
export class PaginationManager {
/**
* @param {number} initialPage
* @param {number} pageSize
*/
constructor(initialPage = 1, pageSize = 5) {
this.currentPage = initialPage;
this.pageSize = pageSize;
this.totalPages = 1;
this.totalItems = 0;
}
/**
* Actualiza los datos de paginación a partir de la respuesta de la API.
* @param {{page: number, pageSize: number, totalPages: number, totalItems: number}} pageInfo
*/
updateFromResponse(pageInfo) {
this.currentPage = pageInfo.page;
this.pageSize = pageInfo.pageSize;
this.totalPages = pageInfo.totalPages;
this.totalItems = pageInfo.totalItems;
}
canGoNext() {
return this.currentPage < this.totalPages;
}
canGoPrev() {
return this.currentPage > 1;
}
goNext() {
if (this.canGoNext()) {
this.currentPage += 1;
}
return this.currentPage;
}
goPrev() {
if (this.canGoPrev()) {
this.currentPage -= 1;
}
return this.currentPage;
}
goTo(page) {
const p = Math.max(1, Math.min(page, this.totalPages));
this.currentPage = p;
return this.currentPage;
}
}
main.js
Responsabilidad: UI.
Habla con librosService y PaginationManager, y se encarga de:
- renderizar libros,
- controlar botones “Anterior” / “Siguiente”,
- mostrar mensajes de carga / error.
Suponemos un HTML tipo:
<!-- index.html (estructura mínima de ejemplo) -->
<body>
<div id="estado"></div>
<ul id="lista-libros"></ul>
<div id="paginacion">
<button id="prev">Anterior</button>
<span id="info-pagina"></span>
<button id="next">Siguiente</button>
</div>
<script type="module" src="./main.js"></script>
</body>
Y ahora el JS:
// main.js
// Capa de presentación: DOM + eventos + estados de carga/éxito/error.
// Usa librosService y PaginationManager, pero no sabe nada de Fetch directamente.
import { fetchLibrosPage } from "./librosService.js";
import { PaginationManager } from "./paginationManager.js";
// Referencias a elementos del DOM
const estado = document.getElementById("estado");
const lista = document.getElementById("lista-libros");
const btnPrev = document.getElementById("prev");
const btnNext = document.getElementById("next");
const infoPagina = document.getElementById("info-pagina");
// Creamos el gestor de paginación
const paginator = new PaginationManager(1, 5);
// Función para renderizar la lista de libros
function renderLibros(items) {
lista.innerHTML = "";
if (!items || items.length === 0) {
const li = document.createElement("li");
li.textContent = "No hay libros en esta página.";
lista.appendChild(li);
return;
}
items.forEach((libro) => {
const li = document.createElement("li");
li.textContent = `${libro.titulo} — ${libro.autor}`;
lista.appendChild(li);
});
}
// Función para actualizar la UI de botones de paginación
function updatePaginationControls() {
btnPrev.disabled = !paginator.canGoPrev();
btnNext.disabled = !paginator.canGoNext();
infoPagina.textContent = `Página ${paginator.currentPage} de ${paginator.totalPages} (Total: ${paginator.totalItems} libros)`;
}
// Función principal: carga una página concreta de libros y actualiza la UI
async function loadPage(page) {
try {
estado.textContent = "Cargando...";
lista.innerHTML = "";
btnPrev.disabled = true;
btnNext.disabled = true;
// Pedimos la página a la API a través del servicio
const pageData = await fetchLibrosPage(page, paginator.pageSize);
// Actualizamos el estado de paginación con lo que responde el backend
paginator.updateFromResponse(pageData);
// Renderizamos libros
renderLibros(pageData.items);
// Éxito: actualizamos mensaje y controles
estado.textContent = "Datos cargados correctamente.";
updatePaginationControls();
} catch (error) {
console.error("Error al cargar libros:", error);
estado.textContent =
"Error al cargar los datos. Inténtalo de nuevo más tarde.";
lista.innerHTML = "";
}
}
// Listeners para los botones
btnPrev.addEventListener("click", () => {
if (!paginator.canGoPrev()) return;
const newPage = paginator.currentPage - 1;
loadPage(newPage);
});
btnNext.addEventListener("click", () => {
if (!paginator.canGoNext()) return;
const newPage = paginator.currentPage + 1;
loadPage(newPage);
});
// Cargar página inicial
loadPage(paginator.currentPage);
Qué estás consiguiendo con esta estructura
-
Separación clara de responsabilidades
apiClient.js: solo sabe de HTTP, fetch, baseURL, errores.librosService.js: solo sabe de “libros” y de cómo se llama el endpoint.paginationManager.js: solo sabe de números de página, total, prev/next.main.js: solo sabe de DOM, eventos y estados visuales.
-
Código reutilizable
Si mañana necesitas paginar usuarios, productos, pedidos:
- Reutilizas
apiClient.jsypaginationManager.js. - Creas un
usuariosService.jsoproductosService.js. - La UI sería casi la misma, cambiando textos y campos.
- Reutilizas
-
Manejo eficiente de APIs paginadas
- No recalculas lógica de paginación en cada componente.
- No duplicas la construcción de URLs con page/pageSize.
- Tienes un único sitio por donde pasa todo (apiClient), donde puedes:
- añadir cabeceras de autenticación,
- manejar errores globales,
- loguear tiempos de respuesta, etc.
Conclusión: dominar las APIs es dominar el desarrollo moderno
Consumir APIs no es solo hacer peticiones: es entender cómo hablar con otros sistemas, manejar errores de forma inteligente y mantener la seguridad en todo momento. Ya sea desde el navegador o desde tu servidor, saber usar Fetch, Axios, JWT y OAuth te da una ventaja enorme como desarrollador.