En el capítulo anterior, los datos “llegaban” de una API falsa. Ahora aprendes a pedirlos de
verdad a un servidor con fetch. Pero antes de eso, una pieza que va siempre con la red: los
datos no viajan como objetos de JavaScript, sino como texto en un formato llamado JSON.
JSON: datos como texto#
JSON (JavaScript Object Notation) es un formato de texto para intercambiar datos. Se parece muchísimo a cómo escribes un objeto en JavaScript, pero es un string con reglas más estrictas:
- Las claves van siempre entre comillas dobles:
"nombre", nonombre. - Solo admite datos: strings, números, booleanos,
null, arrays y objetos. Nada de funciones, ni comentarios, ni comas finales.
{
"nombre": "Tracer",
"rol": "Daño",
"partidas": 120,
"victorias": 78
}Como JSON es texto y tú trabajas con objetos, necesitas traducir en ambos sentidos. Para eso hay
dos funciones del objeto global JSON:
JSON.stringify(valor)— de objeto a string JSON. Para enviar datos.JSON.parse(texto)— de string JSON a objeto. Para usar datos recibidos.
const heroe = { nombre: 'Tracer', rol: 'Daño' };
// Objeto → texto JSON (lo que enviarías al servidor).
const texto = JSON.stringify(heroe);
// texto → '{"nombre":"Tracer","rol":"Daño"}' (un string)
// Texto JSON → objeto (lo que haces con lo que recibes).
const otra = JSON.parse('{"nombre":"Mercy","rol":"Apoyo"}');
// otra → { nombre: 'Mercy', rol: 'Apoyo' } (un objeto de verdad)
// Mercy
console.log(otra.nombre);Pruébalo: JSON.stringify y JSON.parse#
fetch: pedir datos al servidor#
fetch(url) es la función del navegador para hacer una petición HTTP. Devuelve una promesa
(de ahí todo lo del capítulo anterior). Pero ojo a un detalle que confunde al principio: la
promesa de fetch no se resuelve con los datos, sino con un objeto Response —la
respuesta HTTP: su estado, sus cabeceras—. Para obtener el cuerpo ya parseado, llamas a
response.json(), que es otra promesa.
Por eso un fetch típico tiene dos await:
async function cargarHeroes() {
// 1) Espera la respuesta del servidor. response es un objeto Response.
const response = await fetch('https://api.tuequipo.dev/heroes');
// 2) Lee el cuerpo y conviértelo de JSON a objetos. Otra espera.
const heroes = await response.json();
// 3) heroes ya es un array normal: úsalo como siempre.
console.log(heroes.length);
}(El playground de abajo lo hace de verdad: una petición real con fetch a una API pública
abierta. La mecánica —fetch → response.ok → response.json()— es la misma contra cualquier
servidor.)
El error que todo el mundo olvida: response.ok#
Aquí va la trampa más famosa de fetch: solo rechaza la promesa ante un fallo de red (no hay
conexión, el dominio no existe). Si el servidor responde con un 404 (no encontrado) o un
500 (error del servidor), eso es una respuesta HTTP válida, así que fetch se resuelve con
normalidad. Si no lo compruebas, intentarías leer como datos una página de error.
La respuesta trae response.ok (true si el estado es 2xx) y response.status (el código
numérico). Compruébalo siempre:
async function cargarHeroes() {
const response = await fetch('https://api.tuequipo.dev/heroes');
// fetch NO rechaza ante 404/500: lo detectamos a mano.
if (!response.ok) {
throw new Error('El servidor respondió ' + response.status);
}
return response.json();
}Combinado con try/catch, cubres los dos tipos de fallo: el de red (lo lanza fetch) y el de
HTTP (lo lanzas tú al ver !response.ok).
async function mostrar() {
try {
// puede fallar por red o por HTTP
const heroes = await cargarHeroes();
console.log('Llegaron ' + heroes.length + ' héroes');
} catch (error) {
console.log('No se pudo cargar: ' + error.message);
}
}Enviar datos: una nota sobre POST#
fetch por defecto hace una petición GET (pedir). Para enviar datos (crear un héroe, por
ejemplo) le pasas un segundo argumento con el método, las cabeceras y el cuerpo, que va como
texto JSON (de ahí JSON.stringify).
Una cabecera (header) es metadato que viaja junto a la petición: le dice al servidor cosas
sobre lo que le mandas, sin ser el dato en sí. Content-Type: application/json significa
exactamente “el cuerpo de esta petición es texto JSON”: así el servidor sabe cómo interpretarlo.
// Conceptual: enviar un héroe nuevo al servidor.
await fetch('https://api.tuequipo.dev/heroes', {
// el verbo: crear
method: 'POST',
// metadato: le indica al servidor que el cuerpo es JSON
headers: { 'Content-Type': 'application/json' },
// objeto → texto JSON
body: JSON.stringify({ nombre: 'Echo', rol: 'Daño' }),
});Lo verás a fondo cuando montes un backend; por ahora, quédate con que recibir es response.json()
y enviar es body: JSON.stringify(...).
Pruébalo: fetch de verdad#
Esto hace una petición real con fetch a una API pública (una lista de usuarios de ejemplo) y
muestra los primeros en la consola. Rompe la URL —quítale una letra al dominio— y verás cómo
fetch rechaza por fallo de red y el catch lo recoge. Pulsa Ejecutar (o Ctrl+Enter) para lanzar la petición.
Comprueba lo que sabes#
Pregunta 1 de 5
¿Qué es JSON?
Tu turno#
Pide los héroes a la API con fetch, conviértelos de JSON a objetos y muéstralos con su winrate
por la consola. Comprueba response.ok y controla los errores. Cuando lo tengas (o si te
atascas), despliega las soluciones y fíjate en cómo el nivel Excelente aísla la llamada y da forma
a los datos.
Ejercicio · en esta página
Pide los héroes con fetch
Pide los héroes a la API (URL_HEROES) con fetch, conviértelos de JSON a objetos con response.json() y muéstralos con su winrate por la consola. Controla los errores, comprobando también response.ok.
Paso 1: Que funcione
- Pides los datos con fetch y los conviertes con response.json().
- Muestras los héroes por la consola al llegar.
- Atrapas un posible error de red.
Paso 2: Que esté pulido
- Usas async/await para las dos esperas.
- Compruebas response.ok antes de leer el cuerpo.
- Controlas los errores con try/catch.
Paso 3: Que sea excelente
- Aíslas la llamada en una función reutilizable (getJSON).
- Transformas los datos del servidor a lo que la vista necesita, una sola vez.
- Mantienes inmutabilidad y separas cálculo de presentación.
Ver soluciones
// ════════════════════════════════════════════════════════════════════════════
// NIVEL OK — "que funcione"
//
// La mecánica de fetch con .then: fetch(url) da un Response; response.json() lo
// convierte de JSON a objetos (otra promesa); cuando llega, muestras en consola.
// Un .catch mínimo atrapa el fallo de red.
//
// Su límite (lo pule Mejor): no comprueba response.ok, así que un error HTTP
// (404, 500) pasaría como si nada e intentaría leer un cuerpo que no toca; y
// encadenar .then es más farragoso que async/await.
// ════════════════════════════════════════════════════════════════════════════
// ── La "URL del servidor" ──────────────────────────────────────────────────
const heroes = [
{ nombre: "Tracer", rol: "Daño", partidas: 120, victorias: 78 },
{ nombre: "Mercy", rol: "Apoyo", partidas: 200, victorias: 130 },
{ nombre: "Reinhardt", rol: "Tanque", partidas: 90, victorias: 51 },
{ nombre: "Ana", rol: "Apoyo", partidas: 110, victorias: 66 },
];
const URL_HEROES =
"data:application/json," + encodeURIComponent(JSON.stringify(heroes));
// ── Solución ────────────────────────────────────────────────────────────────
function mostrar(lista) {
console.log("Héroes cargados: " + lista.length);
lista.forEach(function (h) {
var winrate = ((h.victorias / h.partidas) * 100).toFixed(1);
console.log(h.nombre + " (" + h.rol + ") — " + winrate + "%");
});
}
// fetch da un Response; .json() lo convierte de JSON a objetos.
// .then encadena el siguiente paso cuando la promesa se resuelve.
fetch(URL_HEROES)
// devuelve OTRA promesa: el cuerpo parseado
.then(function (response) {
return response.json();
})
.then(function (lista) {
mostrar(lista);
})
.catch(function (error) {
console.log("No se pudieron cargar los héroes: " + error.message);
}); Por qué este nivel
- La mecánica con .then: fetch da un Response, response.json() lo convierte de JSON a objetos, y muestras en consola al llegar.
- Un .catch atrapa el fallo de red. Funciona.
- Su límite: no comprueba response.ok, así que un 404/500 pasaría como si fueran datos buenos.
// ════════════════════════════════════════════════════════════════════════════
// NIVEL MEJOR — "que esté pulido"
//
// Por qué mejora a OK:
// - async/await: las dos esperas (el fetch y el .json()) se leen en orden, en
// variables normales, sin .then anidados.
// - Comprueba response.ok: fetch NO rechaza ante un 404 o un 500 (solo ante un
// fallo de red). Si no compruebas el estado, tratarías una página de error
// como si fueran datos buenos. Aquí, si !response.ok, lanzamos un error claro.
// - try/catch para cualquier fallo (red o HTTP).
//
// Qué deja para Excelente: aislar la llamada en una función reutilizable
// (getJSON) y transformar los datos del servidor a lo que la vista necesita.
// ════════════════════════════════════════════════════════════════════════════
const heroes = [
{ nombre: "Tracer", rol: "Daño", partidas: 120, victorias: 78 },
{ nombre: "Mercy", rol: "Apoyo", partidas: 200, victorias: 130 },
{ nombre: "Reinhardt", rol: "Tanque", partidas: 90, victorias: 51 },
{ nombre: "Ana", rol: "Apoyo", partidas: 110, victorias: 66 },
];
const URL_HEROES =
"data:application/json," + encodeURIComponent(JSON.stringify(heroes));
function mostrar(lista) {
console.log("Héroes cargados: " + lista.length);
lista.forEach(function (h) {
var winrate = ((h.victorias / h.partidas) * 100).toFixed(1);
console.log(h.nombre + " (" + h.rol + ") — " + winrate + "%");
});
}
async function cargar() {
try {
// primera espera: la respuesta del servidor
var response = await fetch(URL_HEROES);
// fetch no rechaza ante 404/500: hay que mirar el estado a mano.
if (!response.ok) {
throw new Error("El servidor respondió " + response.status);
}
// segunda espera: leer y parsear el cuerpo
var lista = await response.json();
mostrar(lista);
} catch (error) {
console.log("No se pudieron cargar los héroes: " + error.message);
}
}
cargar(); Por qué es mejor que el anterior
- async/await: las dos esperas (fetch y .json()) se leen en orden, sin .then anidados.
- Comprueba response.ok: fetch no rechaza ante 404/500, así que lo detecta a mano y lanza un error claro.
- try/catch para red y HTTP, con estado de carga visible.
// ════════════════════════════════════════════════════════════════════════════
// NIVEL EXCELENTE — "óptimo"
//
// Por qué mejora a Mejor:
// - getJSON: una función reutilizable que encapsula el patrón completo (fetch +
// comprobar ok + .json()). Toda llamada a la API pasa por aquí, así que la
// comprobación de errores vive en UN sitio y no se te olvida en ninguna.
// - Transforma los datos del servidor (DTO) a lo que la vista necesita
// (ViewModel): calcula el winrate UNA vez, al recibir, no en cada uso.
// - Funciones puras para el cálculo; el mostrado solo lee datos ya listos.
// - Inmutabilidad: enriquece con objetos nuevos, sin tocar la respuesta.
//
// Idea de fondo: la frontera con el servidor es un buen sitio para validar y dar
// forma a los datos UNA vez. A partir de ahí, el resto de la app trabaja con
// datos limpios y predecibles.
// ════════════════════════════════════════════════════════════════════════════
const heroes = [
{ nombre: "Tracer", rol: "Daño", partidas: 120, victorias: 78 },
{ nombre: "Mercy", rol: "Apoyo", partidas: 200, victorias: 130 },
{ nombre: "Reinhardt", rol: "Tanque", partidas: 90, victorias: 51 },
{ nombre: "Ana", rol: "Apoyo", partidas: 110, victorias: 66 },
];
const URL_HEROES =
"data:application/json," + encodeURIComponent(JSON.stringify(heroes));
// ─── Cliente HTTP reutilizable: el patrón fetch en un solo sitio ────────────
async function getJSON(url) {
var response = await fetch(url);
// fetch no rechaza ante errores HTTP: comprobamos el estado nosotros.
if (!response.ok) {
throw new Error("HTTP " + response.status + " al pedir " + url);
}
return response.json();
}
// ─── Transformación DTO -> ViewModel: winrate calculado una vez ─────────────
function conWinrate(lista) {
return lista.map(function (h) {
return {
nombre: h.nombre,
rol: h.rol,
// winrate formateado: se calcula aquí, no en cada console.log
winratePct: ((h.victorias / h.partidas) * 100).toFixed(1) + "%",
};
});
}
// ─── Mostrado: solo lee datos ya calculados ─────────────────────────────────
function mostrar(lista) {
console.log("Héroes cargados: " + lista.length);
lista.forEach(function (h) {
console.log(h.nombre + " (" + h.rol + ") — " + h.winratePct);
});
}
async function cargar() {
try {
// pide y parsea, con errores cubiertos en getJSON
var datos = await getJSON(URL_HEROES);
// transforma una vez y muestra
mostrar(conWinrate(datos));
} catch (error) {
console.log("No se pudieron cargar los héroes: " + error.message);
}
}
cargar(); Por qué es mejor que el anterior
- getJSON: una función reutilizable encapsula fetch + comprobar ok + .json(). El manejo de errores vive en un sitio.
- Transforma los datos del servidor (DTO) a lo que la vista necesita (winrate calculado una vez), de forma inmutable.
- La frontera con el servidor da forma a los datos UNA vez; el resto de la app trabaja con datos limpios.