La lección anterior resolvió media promesa: Nómada Tareas ya sobrevive a F5. Pero Marta, Iván y Lucía siguen teniendo cada uno su propio localStorage, con su propia versión del tablero, sin manera de verse. La otra mitad —que el tablero sea el mismo para todo el equipo— exige que los datos vivan en un servidor y que el navegador sepa hablar con él sin recargar la página. Eso es AJAX, y la herramienta moderna para hacerlo es fetch. En esta lección aprenderás qué cambió AJAX en la web, repasarás el HTTP que necesitas de verdad (verbos, códigos y cabeceras), dominarás fetch y el objeto Response con su trampa más famosa —un 404 no rechaza la promesa—, enviarás datos en JSON y en FormData, construirás URLs con parámetros, entenderás CORS lo suficiente para no perder una tarde, y escribirás js/datos/api-tareas.js, el hermano de red del repositorio local. Y por fin retirarás aquellas leerBacklogSimulado() y guardarInformeSimulado() de 05-06 que fingían latencia con setTimeout.
Contenido
- Qué es AJAX y qué cambió
XMLHttpRequest, el antepasado- HTTP en diez minutos: verbos, rutas, códigos y cabeceras
fetch: la petición mínima- El objeto
Responsey los métodos de cuerpo - La trampa fundamental:
fetchno rechaza con 404 ni 500 - Enviar datos:
method,headersybody - Enviar
FormDatay archivos - Parámetros de consulta con
URLyURLSearchParams - CORS: por qué el navegador te bloquea
- Autenticación:
Authorizationycredentials - Nómada Tareas:
js/datos/api-tareas.js - Practicar de verdad:
json-serveren tu máquina - Errores Comunes y Consejos
- Ejercicios
- Conclusión
- Qué es AJAX y qué cambió
AJAX son las siglas de Asynchronous JavaScript And XML, un nombre acuñado en 2005 que hoy es medio mentira: casi nadie usa XML —se usa JSON— y la técnica se aplica a mucho más que XML. Lo que la sigla nombra sigue vigente: pedir datos al servidor desde JavaScript, sin recargar la página, y actualizar solo la parte del DOM que cambia.
Compara los dos modelos:
| Web clásica (sin AJAX) | Web con AJAX | |
|---|---|---|
| Al pulsar "marcar como hecha" | El navegador envía un formulario y recarga la página entera | JavaScript envía una petición en segundo plano |
| Lo que viaja de vuelta | Un documento HTML completo | Unos cientos de bytes de JSON |
| Estado de la interfaz | Se pierde: scroll, foco, filtros | Se conserva |
| Percepción del usuario | Parpadeo blanco, espera | La tarjeta cambia y ya |
| Trabajo del servidor | Renderizar toda la página | Devolver el dato |
Lo que AJAX hace posible, en términos de Nómada Tareas: Marta pulsa el botón de avanzar estado, la tarjeta se mueve de columna, y ni el scroll ni el filtro por responsable ni el foco se pierden. El ciclo estado → render → evento → nuevo estado → render que montaste en 06-06 sigue mandando; lo único que cambia es que ahora una parte del "nuevo estado" llega del servidor.
Y hay algo que no cambia y conviene decir pronto: el servidor sigue mandando. Una petición AJAX no es más segura que un formulario; el usuario puede fabricar la que quiera desde la consola. Toda validación del cliente es una cortesía; la de verdad está en el servidor.
XMLHttpRequest, el antepasado
XMLHttpRequest, el antepasadoAntes de fetch había XMLHttpRequest (XHR), disponible desde principios de los 2000. Lo verás en código antiguo y merece la pena reconocerlo:
// Estilo XHR: eventos y estados numéricos, sin promesas
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.tallernomada.example/v1/tareas');
xhr.onload = function () {
if (xhr.status >= 200 && xhr.status < 300) {
const tareas = JSON.parse(xhr.responseText); // el parseo, a mano
console.log(tareas.length);
} else {
console.error('Error HTTP', xhr.status);
}
};
xhr.onerror = function () { console.error('Fallo de red'); };
xhr.send();Es el estilo de callbacks de 05-05 llevado al extremo: nada de promesas, nada de async/await, y el famoso readyState con sus cinco valores numéricos. La comparación:
XMLHttpRequest |
fetch |
|
|---|---|---|
| Modelo | Eventos y callbacks | Promesas (05-06) |
| Sintaxis | open + send + onload |
Una llamada que devuelve una promesa |
| Parseo de JSON | Manual (JSON.parse(responseText)) |
await respuesta.json() |
| Errores HTTP (404, 500) | Miras xhr.status |
Tampoco rechaza: miras respuesta.ok |
| Cancelación | xhr.abort() |
AbortController (07-03) |
| Progreso de subida | Sí, evento progress |
No (solo de descarga, con streams) |
| Streaming de la respuesta | Limitado | Sí, respuesta.body es un ReadableStream |
| Timeout integrado | Sí, xhr.timeout |
No, hay que montarlo (07-03) |
Hoy se usa fetch salvo en dos casos: cuando necesitas una barra de progreso de subida de un archivo grande, o cuando mantienes código antiguo. Los dos huecos de fetch —timeout y cancelación— se cubren con AbortController, y esa es la lección siguiente.
- HTTP en diez minutos: verbos, rutas, códigos y cabeceras
Una petición HTTP tiene cuatro partes: un verbo, una ruta, unas cabeceras y a veces un cuerpo. La respuesta tiene un código de estado, sus propias cabeceras y su cuerpo.
Los verbos
| Verbo | Qué significa | ¿Lleva cuerpo? | ¿Idempotente? | En Nómada Tareas |
|---|---|---|---|---|
GET |
Leer un recurso | No | Sí | Listar tareas, leer una tarea |
POST |
Crear un recurso nuevo | Sí | No | Crear una tarea |
PUT |
Reemplazar un recurso entero | Sí | Sí | Guardar una tarea con todos sus campos |
PATCH |
Modificar parte de un recurso | Sí | Depende | Cambiar solo el estado |
DELETE |
Borrar un recurso | No suele | Sí | Eliminar una tarea |
Idempotente significa que repetir la misma petición deja el sistema igual que hacerla una vez. PUT de la tarea 6 con los mismos datos, mil veces, deja una tarea 6 con esos datos. POST mil veces crea mil tareas. Esa distinción parece teórica hasta que en 07-03 decidas qué peticiones se pueden reintentar sin miedo.
Las rutas de recurso
Una API REST nombra cosas, no acciones, y deja que el verbo diga qué hacer con ellas:
GET /v1/tareas → la colección entera GET /v1/tareas?estado=hecha → la colección filtrada GET /v1/tareas/6 → un elemento concreto POST /v1/tareas → crear uno nuevo (el id lo asigna el servidor) PUT /v1/tareas/6 → reemplazar el 6 entero PATCH /v1/tareas/6 → cambiar parte del 6 DELETE /v1/tareas/6 → borrar el 6
Un antipatrón habitual es POST /v1/crearTarea o GET /v1/borrarTarea?id=6. El segundo es especialmente malo: un GET no debe cambiar nada, y cualquier buscador o precargador del navegador podría dispararlo.
El dominio
https://api.tallernomada.example/v1que usaremos en toda la lección es ficticio. El TLD.exampleestá reservado por la IANA precisamente para documentación y jamás resolverá. En el apartado 13 montarás un servidor real en tu máquina para practicar.
Los códigos de estado
Se agrupan en familias, y con saber las familias vas sobrado:
| Familia | Significado | Los que verás |
|---|---|---|
| 1xx | Informativo | 101 Switching Protocols (lo verás en WebSockets, 07-04) |
| 2xx | Éxito | 200 OK, 201 Created (tras un POST), 204 No Content (tras un DELETE) |
| 3xx | Redirección | 301 permanente, 304 Not Modified (caché) |
| 4xx | Error del cliente: la petición está mal | 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 409 Conflict, 422 Unprocessable Entity, 429 Too Many Requests |
| 5xx | Error del servidor: la petición estaba bien, el servidor falló | 500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout |
La frontera entre 4xx y 5xx es la que gobierna tu manejo de errores: un 4xx lo arregla el cliente (corrige los datos, inicia sesión, deja de insistir); un 5xx no lo arregla el cliente (reintenta más tarde y avisa). Dos parejas que se confunden a menudo:
401 Unauthorizedsignifica "no sé quién eres" (falta o caducó la credencial).403 Forbiddensignifica "sé quién eres y no puedes". El primero pide iniciar sesión; el segundo, no.204 No Contentes un éxito sin cuerpo. Llamar a.json()sobre él lanza, porque no hay nada que parsear. Lo tendrás en cuenta enborrarTarea().
Las cabeceras
| Cabecera | Quién la pone | Para qué |
|---|---|---|
Content-Type |
Quien envía cuerpo | Qué formato lleva el cuerpo: application/json, multipart/form-data… |
Accept |
El cliente | Qué formatos entiende de vuelta: application/json |
Authorization |
El cliente | La credencial: Bearer <token> |
Cache-Control |
Ambos | Cómo puede cachearse |
Content-Length |
Quien envía cuerpo | Tamaño en bytes (lo pone el navegador) |
ETag / If-None-Match |
Servidor / cliente | Versión del recurso, para ahorrar transferencia |
El error de novato más frecuente con cabeceras: enviar un body con JSON sin Content-Type: application/json. Muchos servidores lo interpretan como texto plano y devuelven un 400 desconcertante.
fetch: la petición mínima
fetch: la petición mínimafetch(url, opciones) devuelve una promesa que se resuelve con un objeto Response en cuanto llegan las cabeceras de la respuesta. Con lo que sabes de 05-06, se lee sin esfuerzo:
// Con async/await, que es como lo escribirás siempre
async function listar() {
const respuesta = await fetch('https://api.tallernomada.example/v1/tareas');
const tareas = await respuesta.json();
console.log(tareas.length);
}Dos await, y cada uno espera algo distinto. Esto es lo que más cuesta al principio:
sequenceDiagram
participant JS as Tu código
participant N as Navegador
participant S as API
JS->>N: fetch(url)
N->>S: GET /v1/tareas
Note over N,S: Latencia de red
S-->>N: 200 OK + cabeceras
N-->>JS: ✅ promesa resuelta con Response
Note over JS: El CUERPO aún no ha llegado
JS->>N: respuesta.json()
N-->>JS: (lee el cuerpo, lo parsea)
N-->>JS: ✅ segunda promesa resuelta con los datos
La primera promesa se resuelve con las cabeceras; el cuerpo puede seguir llegando. Por eso respuesta.json() es otra operación asíncrona y necesita su propio await. Olvidarlo produce el error más común de todos:
const respuesta = await fetch(url);
const datos = respuesta.json(); // ✗ falta el await
console.log(datos.length); // undefined ← es una Promise, no un array
- El objeto
Response y los métodos de cuerpo
Response y los métodos de cuerpoResponse describe la respuesta completa:
| Miembro | Tipo | Qué contiene |
|---|---|---|
ok |
boolean |
true si el estado está entre 200 y 299 |
status |
number |
El código: 200, 404, 500… |
statusText |
string |
El texto: 'OK', 'Not Found' |
headers |
Headers |
Las cabeceras, con get, has, entries |
url |
string |
La URL final (tras redirecciones) |
redirected |
boolean |
Si hubo redirección |
type |
string |
'basic', 'cors', 'opaque'… |
bodyUsed |
boolean |
Si el cuerpo ya se consumió |
Y los métodos de cuerpo, todos asíncronos porque todos devuelven promesas:
| Método | Devuelve | Cuándo |
|---|---|---|
json() |
El valor parseado | Respuestas JSON. Lanza si el cuerpo no es JSON válido |
text() |
string |
HTML, CSV, texto, o para depurar qué llegó de verdad |
blob() |
Blob |
Imágenes, PDF, cualquier binario |
arrayBuffer() |
ArrayBuffer |
Binario a bajo nivel (lo usarás en 07-07 con Wasm) |
formData() |
FormData |
Respuestas multipart o urlencoded |
const respuesta = await fetch('https://api.tallernomada.example/v1/tareas');
console.log(respuesta.ok); // true
console.log(respuesta.status, respuesta.statusText); // 200 'OK'
console.log(respuesta.headers.get('content-type')); // 'application/json; charset=utf-8'
for (const [nombre, valor] of respuesta.headers) {
console.log(nombre, '=', valor);
}Los nombres de cabecera no distinguen mayúsculas: headers.get('Content-Type') y headers.get('content-type') son lo mismo.
Una regla que sorprende: el cuerpo solo se puede leer una vez. Es un flujo, y una vez consumido, se acabó.
const respuesta = await fetch(url);
const texto = await respuesta.text();
const datos = await respuesta.json(); // ✗ TypeError: body stream already readSi necesitas leerlo dos veces —por ejemplo, intentar json() y, si falla, mirar el text() para depurar—, clona antes:
const respuesta = await fetch(url);
const copia = respuesta.clone(); // ← clona ANTES de leer
try {
return await respuesta.json();
} catch {
console.error('La respuesta no era JSON. Contenido real:', await copia.text());
throw new Error('Respuesta no interpretable');
}Ese patrón te salvará el día que un proxy devuelva una página HTML de error donde esperabas JSON.
- La trampa fundamental:
fetch no rechaza con 404 ni 500
fetch no rechaza con 404 ni 500Esta es la cosa que hay que saber de fetch, y la que más código roto ha producido:
La promesa de
fetchsolo se rechaza si la petición no llegó a completarse: red caída, DNS que no resuelve, CORS bloqueado, petición cancelada. Si el servidor responde —aunque responda 404, 403 o 500— la promesa se resuelve con normalidad.
// ✗ Roto: parece correcto y no lo es
async function listarRoto() {
try {
const respuesta = await fetch('https://api.tallernomada.example/v1/tareaz'); // ruta mal escrita
const datos = await respuesta.json(); // el servidor devolvió 404 con un JSON de error
return datos; // devuelve { error: 'No encontrado' } como si fueran tareas
} catch (error) {
console.error('Nunca llego aquí por un 404');
}
}El catch no se ejecuta. respuesta.json() parsea el cuerpo del error sin quejarse, y tu aplicación sigue adelante con basura. La corrección es siempre comprobar ok:
// ✓ Correcto
async function listar() {
const respuesta = await fetch('https://api.tallernomada.example/v1/tareas');
if (!respuesta.ok) { // ← la línea que no puede faltar
throw new Error(`HTTP ${respuesta.status} ${respuesta.statusText}`);
}
return respuesta.json();
}La tabla que explica el porqué del diseño:
| Situación | ¿La promesa de fetch…? |
Cómo lo detectas |
|---|---|---|
200 OK |
se resuelve | respuesta.ok === true |
404 Not Found |
se resuelve | respuesta.ok === false, status 404 |
500 Internal Server Error |
se resuelve | respuesta.ok === false, status 500 |
| Sin conexión / DNS falla | se rechaza | TypeError: Failed to fetch |
| Bloqueado por CORS | se rechaza | TypeError, con detalle solo en la consola |
| Petición cancelada | se rechaza | AbortError (07-03) |
La lógica del diseño es defendible: fetch promete hacer la petición HTTP, y un 404 es una petición HTTP realizada con éxito cuya respuesta es "no existe". Que sea defendible no quita que sea la primera causa de bugs de red. En el apartado 12 encapsularemos la comprobación una sola vez para no volver a olvidarla.
Fíjate además en el detalle de la última fila del diagnóstico: cuando CORS bloquea una petición, JavaScript recibe un TypeError genérico sin detalles. El motivo real solo aparece en la consola del navegador. Es intencionado —dar detalles sería filtrar información entre orígenes— y significa que la consola es tu única fuente de verdad ante un fallo de CORS.
- Enviar datos:
method, headers y body
method, headers y bodyEl segundo parámetro de fetch es un objeto de opciones. Para crear una tarea:
const nueva = {
titulo: 'Revisar la prensa de serigrafía',
responsable: 'Iván',
prioridad: 'alta',
etiquetas: ['serigrafía', 'mantenimiento'],
horasEstimadas: 4,
fechaLimite: '2026-10-10'
};
const respuesta = await fetch('https://api.tallernomada.example/v1/tareas', {
method: 'POST',
headers: {
'Content-Type': 'application/json', // qué envío
'Accept': 'application/json' // qué espero recibir
},
body: JSON.stringify(nueva) // ← el cuerpo SIEMPRE es texto o binario
});
if (!respuesta.ok) throw new Error(`HTTP ${respuesta.status}`);
const creada = await respuesta.json();
console.log(creada.id); // 7 ← el id lo asigna el servidor (R1)Cuatro puntos:
bodynunca es un objeto. Si le pasas{ titulo: '…' }directamente, el navegador lo convierte conString()y envía'[object Object]', exactamente el mismo desastre que enlocalStorage.JSON.stringifyes obligatorio.JSON.stringifyusa eltoJSONde tus instancias, así que puedes pasarle unaTareadirectamente y viajará completa, campos privados incluidos. Otra vez 05-03 rindiendo.- El
idlo asigna el servidor, no el cliente. Es la regla R1 del proyecto, y en un sistema con varios usuarios es la única forma de no colisionar. fetchno lanza si el servidor rechaza los datos. Un422 Unprocessable Entitycon la lista de campos inválidos llega como respuesta normal; hay que comprobaroky leer el cuerpo del error.
Las opciones que más usarás:
| Opción | Valores | Para qué |
|---|---|---|
method |
'GET', 'POST', 'PUT', 'PATCH', 'DELETE' |
El verbo (por defecto 'GET') |
headers |
Objeto o Headers |
Las cabeceras |
body |
string, FormData, Blob, URLSearchParams |
El cuerpo |
credentials |
'omit', 'same-origin', 'include' |
Si van cookies |
mode |
'cors', 'same-origin', 'no-cors' |
Política de origen cruzado |
cache |
'default', 'no-store', 'reload'… |
Uso de la caché HTTP |
signal |
AbortSignal |
Cancelación (07-03) |
redirect |
'follow', 'error', 'manual' |
Qué hacer con los 3xx |
- Enviar
FormData y archivos
FormData y archivosCuando hay un archivo de por medio —Iván quiere adjuntar la foto del boceto—, JSON no sirve: es texto. Se usa FormData, que ya conoces de 06-07:
const formulario = document.querySelector('#nueva-tarea');
const datos = new FormData(formulario); // toma todos los campos con name
datos.append('adjunto', archivoInput.files[0]);
datos.append('origen', 'web');
const respuesta = await fetch('https://api.tallernomada.example/v1/tareas', {
method: 'POST',
body: datos // ← ¡SIN Content-Type!
});No pongas Content-Type cuando envías FormData. El navegador lo genera solo, y añade el boundary —un separador aleatorio— que el servidor necesita para trocear el cuerpo. Si lo escribes tú, el boundary falta y el servidor no puede leer nada. Es un fallo desconcertante, porque el código "parece más completo" con la cabecera puesta.
Comparación rápida de los tres formatos de cuerpo:
| Formato | Content-Type |
¿Lo pones tú? | Cuándo |
|---|---|---|---|
| JSON | application/json |
Sí | Lo normal en una API |
FormData |
multipart/form-data; boundary=… |
No | Archivos, o formularios tal cual |
URLSearchParams |
application/x-www-form-urlencoded |
No | APIs antiguas, formularios simples |
// URLSearchParams como cuerpo: el navegador pone el Content-Type correcto
const cuerpo = new URLSearchParams({ estado: 'hecha', revisor: 'Marta' });
await fetch('https://api.tallernomada.example/v1/tareas/6', { method: 'PATCH', body: cuerpo });
- Parámetros de consulta con
URL y URLSearchParams
URL y URLSearchParamsConstruir la cadena de consulta a mano es una fuente inagotable de bugs de codificación:
// ✗ Frágil: ¿y si el texto tiene un espacio, un & o una ñ?
const url = `https://api.tallernomada.example/v1/tareas?responsable=${responsable}&texto=${texto}`;
// Con responsable = 'Lucía' y texto = 'prensa & rodillo' → URL rotaLas clases URL y URLSearchParams codifican por ti:
const url = new URL('https://api.tallernomada.example/v1/tareas');
url.searchParams.set('responsable', 'Lucía');
url.searchParams.set('estado', 'pendiente');
url.searchParams.set('texto', 'prensa & rodillo');
url.searchParams.set('limite', 20);
console.log(url.toString());
// https://api.tallernomada.example/v1/tareas?responsable=Luc%C3%ADa&estado=pendiente&texto=prensa+%26+rodillo&limite=20
const respuesta = await fetch(url); // fetch acepta un objeto URL, sin toString()Los miembros útiles de URLSearchParams:
| Método | Qué hace |
|---|---|
set(clave, valor) |
Pone el valor, reemplazando los que hubiera |
append(clave, valor) |
Añade otro valor con la misma clave (?etiqueta=a&etiqueta=b) |
get(clave) / getAll(clave) |
Lee el primero / todos |
has(clave) |
Si existe |
delete(clave) |
Lo quita |
toString() |
La cadena codificada, sin la ? |
Un ayudante que usarás en el proyecto, saltándose los filtros vacíos:
/** Construye una URL de la API con solo los parámetros que tienen valor. */
function urlDeApi(ruta, parametros = {}) {
const url = new URL(ruta, BASE);
for (const [clave, valor] of Object.entries(parametros)) {
if (valor === undefined || valor === null || valor === '') continue; // filtro vacío = sin filtro
url.searchParams.set(clave, valor);
}
return url;
}
urlDeApi('/v1/tareas', { responsable: 'Iván', estado: '', texto: null });
// https://api.tallernomada.example/v1/tareas?responsable=Iv%C3%A1nEse new URL(ruta, BASE) con dos argumentos resuelve rutas relativas contra una base, igual que lo hace un <a href>. Muy cómodo para no concatenar barras.
- CORS: por qué el navegador te bloquea
Antes o después escribirás una petición perfecta y la consola dirá algo como:
Access to fetch at 'https://api.tallernomada.example/v1/tareas' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
CORS (Cross-Origin Resource Sharing) es el mecanismo que decide si una página de un origen puede leer respuestas de otro origen. Por defecto, el navegador aplica la same-origin policy: puedes enviar peticiones a otros orígenes, pero no leer sus respuestas salvo que ese servidor lo autorice.
¿Por qué existe? Porque el navegador envía automáticamente las cookies del usuario. Sin esta política, cualquier página maliciosa que visitaras podría pedir https://tubanco.example/api/saldo con tus cookies y leer la respuesta.
Hay dos tipos de petición cruzada, y la diferencia importa mucho:
Petición simple: el navegador la envía directamente y luego decide si te deja leer la respuesta. Se considera simple si usa GET, HEAD o POST, y sus cabeceras son las corrientes, con un Content-Type limitado a text/plain, multipart/form-data o application/x-www-form-urlencoded.
sequenceDiagram
participant P as Página<br/>localhost:3000
participant N as Navegador
participant S as api.tallernomada.example
P->>N: fetch('https://api…/v1/tareas')
N->>S: GET /v1/tareas<br/>Origin: http://localhost:3000
S-->>N: 200 OK<br/>Access-Control-Allow-Origin: http://localhost:3000
N->>N: ¿La cabecera autoriza mi origen?
N-->>P: ✅ Respuesta entregada
Petición con preflight: cualquier otra cosa —PUT, PATCH, DELETE, o un POST con Content-Type: application/json, o una cabecera Authorization— obliga al navegador a preguntar antes con una petición OPTIONS.
sequenceDiagram
participant P as Página<br/>localhost:3000
participant N as Navegador
participant S as api.tallernomada.example
P->>N: fetch(url, { method: 'PATCH', headers: {…} })
Note over N: PATCH + Content-Type JSON<br/>⇒ hace falta preflight
N->>S: OPTIONS /v1/tareas/6<br/>Origin: http://localhost:3000<br/>Access-Control-Request-Method: PATCH<br/>Access-Control-Request-Headers: content-type
S-->>N: 204 No Content<br/>Access-Control-Allow-Origin: http://localhost:3000<br/>Access-Control-Allow-Methods: GET, POST, PATCH, DELETE<br/>Access-Control-Allow-Headers: content-type<br/>Access-Control-Max-Age: 86400
N->>N: Autorizado ✅
N->>S: PATCH /v1/tareas/6 (la petición real)
S-->>N: 200 OK + Access-Control-Allow-Origin
N-->>P: ✅ Respuesta entregada
Ese OPTIONS extra explica dos cosas que desconciertan: por qué en la pestaña Network aparecen dos entradas por una sola llamada, y por qué a veces el fallo ocurre "antes" de que el servidor vea tu petición real.
Las cabeceras que controla el servidor:
| Cabecera de respuesta | Qué autoriza |
|---|---|
Access-Control-Allow-Origin |
Qué origen puede leer (https://app.taller.example o *) |
Access-Control-Allow-Methods |
Qué verbos se permiten |
Access-Control-Allow-Headers |
Qué cabeceras puede enviar el cliente |
Access-Control-Allow-Credentials |
Si se pueden enviar cookies (true) |
Access-Control-Expose-Headers |
Qué cabeceras de respuesta puede leer tu JavaScript |
Access-Control-Max-Age |
Cuántos segundos cachear el preflight |
Y la conclusión que ahorra tardes enteras:
CORS no se arregla desde el cliente. Ni con
mode, ni con cabeceras, ni con trucos. Lo aplica el navegador según lo que responde el servidor. Solo hay tres salidas legítimas: que el servidor añada las cabeceras, poner un proxy en tu mismo origen que reenvíe las peticiones, o servir API y web bajo el mismo origen.
Dos matices que se malinterpretan:
mode: 'no-cors'no desactiva CORS. Te devuelve una respuesta opaca:status0,okfalse y cuerpo ilegible. Sirve para casos muy concretos (precachear en un service worker), no para saltarse nada.- Las extensiones tipo "Allow CORS" del navegador solo desactivan la comprobación en tu máquina. Tu código seguirá roto para todos los demás. Úsalas, como mucho, para diagnosticar.
Access-Control-Allow-Origin: *es incompatible con credenciales. Si envías cookies, el servidor debe nombrar tu origen exacto.
- Autenticación:
Authorization y credentials
Authorization y credentialsHay dos formas de que la API sepa quién eres.
Token en la cabecera Authorization, el patrón habitual de las APIs:
const respuesta = await fetch('https://api.tallernomada.example/v1/tareas', {
headers: { 'Authorization': `Bearer ${token}` }
});Cookies, que el navegador gestiona solo. Con fetch hay que pedirlas explícitamente cuando la API está en otro origen:
credentials |
Comportamiento |
|---|---|
'same-origin' |
Por defecto: cookies solo si la URL es del mismo origen |
'include' |
Cookies siempre, también entre orígenes (requiere Access-Control-Allow-Credentials: true) |
'omit' |
Nunca envía cookies |
¿Cuál elegir? Retomando la advertencia de 07-01:
Token en localStorage |
Cookie HttpOnly |
|
|---|---|---|
| ¿Lo lee un XSS? | Sí, entero | No, JavaScript no la ve |
| ¿Se envía solo? | No, lo pones tú en cada petición | Sí, el navegador |
| Riesgo de CSRF | Bajo | Existe; se mitiga con SameSite y tokens anti-CSRF |
| Caducidad | La gestionas tú | El servidor, con Max-Age |
La recomendación no ha cambiado: para una sesión de usuario real, cookie HttpOnly; Secure; SameSite=Lax, emitida y validada por el servidor. Si tu API impone un token en localStorage —cosa habitual—, asume que un XSS es un robo de cuenta y trata la prevención de XSS (06-02: textContent en lugar de innerHTML) como parte de la seguridad de la autenticación.
Tres reglas más, cortas y no negociables:
- HTTPS siempre. Sobre
http://, cabeceras y cuerpos viajan legibles para cualquiera en la red. Un token enviado por HTTP es un token público. - Nunca metas claves de API en el código del cliente. Todo lo que llega al navegador es visible. Si una clave debe permanecer secreta, la petición la hace tu servidor, no la página.
- Ante un
401, limpia la sesión local y manda a iniciar sesión. Reintentar con un token caducado solo genera ruido.
- Nómada Tareas:
js/datos/api-tareas.js
js/datos/api-tareas.jsYa puedes escribir el hermano de red de RepositorioLocal. La misma responsabilidad —traducir entre el mundo exterior y el modelo— con otro medio.
// js/datos/api-tareas.js
import { Tarea } from '../modelo/tarea.js';
/**
* API ficticia de ejemplo. El TLD .example está reservado y NUNCA resuelve:
* para practicar de verdad, arranca el json-server del apartado 13 y cambia esta
* constante por 'http://localhost:3000'.
*/
const BASE = 'https://api.tallernomada.example/v1';
const CABECERAS_JSON = {
'Content-Type': 'application/json',
'Accept': 'application/json'
};
/** Une la base con la ruta y añade solo los parámetros con valor. */
function construirUrl(ruta, parametros = {}) {
const url = new URL(BASE + ruta);
for (const [clave, valor] of Object.entries(parametros)) {
if (valor === undefined || valor === null || valor === '') continue;
url.searchParams.set(clave, valor);
}
return url;
}
/**
* Comprobación única de `ok` para toda la aplicación: escrita una vez, imposible de olvidar.
* En 07-03 esta función crecerá hasta convertirse en `pedirJson`, con su ErrorDeApi.
*/
async function comprobar(respuesta) {
if (respuesta.ok) return respuesta;
const detalle = await respuesta.text().catch(() => '');
throw new Error(`HTTP ${respuesta.status} ${respuesta.statusText}${detalle ? ` — ${detalle}` : ''}`);
}
/** GET /v1/tareas → array de instancias Tarea (no de objetos planos). */
export async function listarTareas({ responsable, estado, texto } = {}) {
const respuesta = await fetch(construirUrl('/tareas', { responsable, estado, texto }), {
headers: { 'Accept': 'application/json' }
});
await comprobar(respuesta);
const planas = await respuesta.json();
return planas.map((datos) => Tarea.desdeJSON(datos)); // ← misma frontera que en 07-01
}
/** GET /v1/tareas/:id */
export async function obtenerTarea(id) {
const respuesta = await fetch(construirUrl(`/tareas/${id}`), {
headers: { 'Accept': 'application/json' }
});
await comprobar(respuesta);
return Tarea.desdeJSON(await respuesta.json());
}
/** POST /v1/tareas → 201 Created con la tarea ya provista de id (R1: lo asigna el servidor). */
export async function crearTarea(datos) {
const respuesta = await fetch(construirUrl('/tareas'), {
method: 'POST',
headers: CABECERAS_JSON,
body: JSON.stringify(datos) // si `datos` es una Tarea, su toJSON() la serializa entera
});
await comprobar(respuesta);
return Tarea.desdeJSON(await respuesta.json());
}
/** PATCH /v1/tareas/:id → cambios parciales; PUT reemplazaría la tarea entera. */
export async function actualizarTarea(id, cambios) {
const respuesta = await fetch(construirUrl(`/tareas/${id}`), {
method: 'PATCH',
headers: CABECERAS_JSON,
body: JSON.stringify(cambios)
});
await comprobar(respuesta);
return Tarea.desdeJSON(await respuesta.json());
}
/** DELETE /v1/tareas/:id → normalmente 204 No Content, SIN cuerpo que parsear. */
export async function borrarTarea(id) {
const respuesta = await fetch(construirUrl(`/tareas/${id}`), { method: 'DELETE' });
await comprobar(respuesta);
return true; // ← nada de respuesta.json(): un 204 no tiene cuerpo
}Cuatro decisiones a subrayar:
- La frontera del modelo se respeta. El módulo devuelve instancias de
Tarea, no objetos planos, exactamente como hacíaRepositorioLocal. El resto de la aplicación no sabe de dónde vienen los datos. Ese es el sentido de tener una carpetadatos/. comprobarestá escrita una sola vez. La trampa del apartado 6 se neutraliza encapsulándola, no recordándola.borrarTareano llama ajson(). Un204 No Contentno tiene cuerpo yjson()lanzaría.- No hay
try/catchaquí. Este módulo traduce y propaga; quien decide qué mostrar al usuario es la vista. Cómo clasificar y presentar esos errores es justo el tema de 07-03.
Y así se sustituyen por fin las funciones simuladas de 05-06:
// js/app.js — antes
// const backlog = await leerBacklogSimulado(); // setTimeout fingiendo latencia
// js/app.js — ahora
import { listarTareas } from './datos/api-tareas.js';
import { RepositorioLocal } from './datos/repositorio-local.js';
const repositorio = new RepositorioLocal();
async function arrancar() {
// 1 · Pintar de inmediato lo que haya en local: la interfaz no espera a la red
const local = repositorio.cargar();
if (local !== null) { vista.actualizar({ tablero: local }); }
// 2 · Traer la verdad del servidor y refrescar
const tareas = await listarTareas();
const tablero = new Tablero('Taller Nómada', tareas);
vista.actualizar({ tablero });
repositorio.guardar(tablero); // ← la copia local queda al día
}
arrancar();Ahí está el patrón que hace que una aplicación se sienta rápida: local primero, red después. El almacenamiento de 07-01 y la red de esta lección no compiten; se complementan. (Ese await desnudo, sin try/catch ni estado de carga, todavía está a medias: lo completarás en la lección siguiente.)
- Practicar de verdad:
json-server en tu máquina
json-server en tu máquinaapi.tallernomada.example no existe. Para practicar necesitas un servidor real, y el más rápido de montar es json-server, que convierte un fichero JSON en una API REST completa.
# 1 · Crea una carpeta para el servidor de pruebas
mkdir nomada-api && cd nomada-api
# 2 · Arráncalo directamente con npx (no hace falta instalar nada permanente)
npx json-server --watch db.json --port 3000Con este db.json, que es el backlog canónico del proyecto:
{
"tareas": [
{ "id": 1, "titulo": "Rediseñar la sala polivalente", "responsable": "Iván",
"prioridad": "alta", "estado": "en-curso", "etiquetas": ["diseño", "espacio"],
"horasEstimadas": 12, "fechaLimite": "2026-09-30", "revisor": "Marta" },
{ "id": 2, "titulo": "Migrar la web del taller", "responsable": "Lucía",
"prioridad": "media", "estado": "pendiente", "etiquetas": ["web"],
"horasEstimadas": 8, "fechaLimite": "2026-10-15", "revisor": "Iván" },
{ "id": 3, "titulo": "Catálogo de encuadernación", "responsable": "Iván",
"prioridad": "media", "estado": "pendiente", "etiquetas": ["diseño", "encuadernación"],
"horasEstimadas": 8, "fechaLimite": "2026-10-05", "revisor": "Marta" },
{ "id": 4, "titulo": "Inventario del almacén de serigrafía", "responsable": "Lucía",
"prioridad": "baja", "estado": "hecha", "etiquetas": ["taller", "inventario"],
"horasEstimadas": 3, "fechaLimite": "2026-09-12", "revisor": "Marta" },
{ "id": 5, "titulo": "Preparar la jornada de puertas abiertas", "responsable": "Marta",
"prioridad": "alta", "estado": "en-curso", "etiquetas": ["evento"],
"horasEstimadas": 6, "fechaLimite": "2026-11-20", "revisor": "Lucía" },
{ "id": 6, "titulo": "Presupuesto de la carpintería", "responsable": "Iván",
"prioridad": "alta", "estado": "pendiente", "etiquetas": ["carpintería", "compras"],
"horasEstimadas": 5, "fechaLimite": "2026-09-05", "revisor": "Marta" }
]
}Ese fichero da inmediatamente todas las rutas que necesitas:
curl http://localhost:3000/tareas
curl http://localhost:3000/tareas/6
curl "http://localhost:3000/tareas?responsable=Iván&estado=pendiente"
curl -X POST http://localhost:3000/tareas \
-H "Content-Type: application/json" \
-d '{"titulo":"Revisar la prensa","responsable":"Iván","horasEstimadas":4}'
curl -X PATCH http://localhost:3000/tareas/6 \
-H "Content-Type: application/json" -d '{"estado":"en-curso"}'
curl -X DELETE http://localhost:3000/tareas/6Solo tienes que cambiar una línea en tu módulo:
Y json-server ya envía Access-Control-Allow-Origin: *, así que no pelearás con CORS mientras aprendes. Otras opciones para practicar: https://jsonplaceholder.typicode.com (API pública de solo lectura simulada), https://httpbin.org (devuelve tu propia petición, ideal para inspeccionar cabeceras) o https://httpstat.us/500 (devuelve el código que le pidas, perfecto para probar el manejo de errores de la lección siguiente).
Errores Comunes y Consejos
- Olvidar comprobar
respuesta.ok. El error número uno. Un 404 llega como éxito y tu aplicación procesa un mensaje de error como si fueran datos. - Olvidar el
awaitderespuesta.json(). Obtienes unaPromisey todo lo que hagas con ella daundefined. - Pasar un objeto como
body. Se convierte en'[object Object]'.JSON.stringifysiempre. - Poner
Content-Typeal enviarFormData. Rompe elboundaryy el servidor no puede leer el cuerpo. - Llamar a
.json()sobre un204. No hay cuerpo; lanza. Compruebastatus === 204oContent-Length. - Leer el cuerpo dos veces.
TypeError: body stream already read. Usarespuesta.clone()antes de la primera lectura. - Concatenar parámetros a mano. Un espacio, un
&o una tilde rompen la URL. UsaURLyURLSearchParams. - Intentar arreglar CORS desde el cliente. No se puede. Lee la consola, habla con quien mantiene la API o monta un proxy.
- Usar
mode: 'no-cors'para "saltarse" CORS. Devuelve una respuesta opaca e ilegible. - Meter una clave de API en el JavaScript del cliente. Es pública desde el momento en que se descarga.
- Consejo: mira siempre la pestaña Network de las DevTools. Verbo, código, cabeceras, cuerpo enviado y recibido, y el
OPTIONSdel preflight. Casi todos los problemas de red se diagnostican ahí en treinta segundos. - Consejo: usa
await respuesta.text()cuandojson()falle. Verás si el servidor devolvió una página HTML de error en lugar de JSON. - Consejo: encapsula
fetchen un único módulo. Nunca lo llames suelto desde la vista. Así la comprobación deok, la base de la URL y las cabeceras viven en un solo sitio, y en 07-03 podrás añadir timeouts y reintentos sin tocar el resto. - Consejo: en el navegador,
copy(await (await fetch(url)).json())en la consola copia la respuesta al portapapeles. Muy útil para inspeccionar formatos.
Ejercicios
Ejercicio 1 — El envoltorio que no se puede olvidar.
Escribe pedirJson(url, opciones = {}) que: haga el fetch; si respuesta.ok es falso, lea el cuerpo como texto y lance un Error cuyo mensaje incluya el status, el statusText y ese texto; si el estado es 204 devuelva null; y en caso contrario devuelva await respuesta.json(). Reescribe después listarTareas y borrarTarea usándolo, y comprueba que el código queda más corto.
Ejercicio 2 — Buscador de tareas con parámetros.
Escribe buscarTareas({ texto, responsable, estados, orden, pagina }) que construya la URL con URLSearchParams, omitiendo los valores vacíos. estados es un array (['pendiente', 'en-curso']) y debe generar ?estado=pendiente&estado=en-curso. La paginación usa _page y _limit, los parámetros de json-server. Devuelve { tareas, total } leyendo el total de la cabecera X-Total-Count.
Ejercicio 3 — Sincronizar el tablero local con el servidor.
Escribe sincronizar(repositorio, api) que: cargue el tablero local; pida al servidor la lista de tareas; y devuelva un informe { soloLocal, soloServidor, enAmbos, coinciden } comparando por id, donde coinciden es el número de tareas presentes en ambos lados con el mismo estado. No modifiques nada todavía: solo informa. Usa Map y los métodos de array de 04-05.
Soluciones
Solución 1
export async function pedirJson(url, opciones = {}) {
const respuesta = await fetch(url, opciones);
if (!respuesta.ok) {
// El cuerpo del error suele traer el detalle útil; si no se puede leer, seguimos igual
const detalle = await respuesta.text().catch(() => '');
throw new Error(`HTTP ${respuesta.status} ${respuesta.statusText}${detalle ? ` — ${detalle}` : ''}`);
}
if (respuesta.status === 204) return null; // sin cuerpo
const tipo = respuesta.headers.get('content-type') ?? '';
if (!tipo.includes('application/json')) {
throw new Error(`Se esperaba JSON y llegó "${tipo}"`); // proxy, login HTML, error del CDN…
}
return respuesta.json();
}export async function listarTareas(filtros = {}) {
const planas = await pedirJson(construirUrl('/tareas', filtros), { headers: { Accept: 'application/json' } });
return planas.map((d) => Tarea.desdeJSON(d));
}
export async function borrarTarea(id) {
await pedirJson(construirUrl(`/tareas/${id}`), { method: 'DELETE' }); // 204 → null, sin romperse
return true;
}La comprobación del content-type es la que evita el peor de los fallos: un proxy corporativo o una pantalla de inicio de sesión devuelven HTML con estado 200, y sin esa línea el json() lanzaría un SyntaxError incomprensible. Esta función es el germen del pedirJson completo que construirás en 07-03.
Solución 2
export async function buscarTareas({ texto = '', responsable = null, estados = [],
orden = 'fechaLimite', pagina = 1, porPagina = 20 } = {}) {
const url = new URL(`${BASE}/tareas`);
if (texto) url.searchParams.set('q', texto); // búsqueda libre de json-server
if (responsable) url.searchParams.set('responsable', responsable);
for (const estado of estados) url.searchParams.append('estado', estado); // append, no set
url.searchParams.set('_sort', orden);
url.searchParams.set('_page', pagina);
url.searchParams.set('_limit', porPagina);
const respuesta = await fetch(url, { headers: { Accept: 'application/json' } });
if (!respuesta.ok) throw new Error(`HTTP ${respuesta.status}`);
const planas = await respuesta.json();
return {
tareas: planas.map((d) => Tarea.desdeJSON(d)),
total: Number(respuesta.headers.get('X-Total-Count') ?? planas.length)
};
}La diferencia entre set y append es la clave: set reemplaza, así que un bucle con set dejaría solo el último estado. Y ojo con X-Total-Count: en una API de otro origen, esa cabecera solo será legible si el servidor la expone con Access-Control-Expose-Headers. Con json-server en localhost no hay problema.
Solución 3
export async function sincronizar(repositorio, api) {
const local = repositorio.cargar();
const tareasLocales = local === null ? [] : local.tareas;
const tareasRemotas = await api.listarTareas();
const porIdLocal = new Map(tareasLocales.map((t) => [t.id, t]));
const porIdRemoto = new Map(tareasRemotas.map((t) => [t.id, t]));
const soloLocal = tareasLocales.filter((t) => !porIdRemoto.has(t.id)).map((t) => t.id);
const soloServidor = tareasRemotas.filter((t) => !porIdLocal.has(t.id)).map((t) => t.id);
const enAmbos = tareasLocales.filter((t) => porIdRemoto.has(t.id)).map((t) => t.id);
const coinciden = enAmbos.filter((id) => porIdLocal.get(id).estado === porIdRemoto.get(id).estado).length;
return { soloLocal, soloServidor, enAmbos, coinciden, conflictos: enAmbos.length - coinciden };
}console.log(await sincronizar(repositorio, api));
// { soloLocal: [7], soloServidor: [], enAmbos: [1,2,3,4,5,6], coinciden: 5, conflictos: 1 }Los dos Map convierten la comparación en operaciones de coste constante en lugar de recorrer el array remoto por cada tarea local. Y fíjate en lo que revela el resultado: hay un conflicto. Qué hacer con él —quién gana cuando local y servidor discrepan— es una decisión de producto, no técnica, y volverás sobre ella en 07-04 al hablar de edición simultánea.
Conclusión
Nómada Tareas ya sabe hablar con un servidor. Entiendes qué significa AJAX y por qué cambió la web: pedir datos en segundo plano y actualizar solo lo que cambia, conservando scroll, foco y filtros, con un JSON de unos cientos de bytes en lugar de un documento HTML completo. Conoces a XMLHttpRequest lo justo para reconocerlo, y sabes que fetch lo sustituye en todo salvo en el progreso de subida, y que sus dos huecos —timeout y cancelación— se rellenan con AbortController.
Tienes el HTTP que se usa a diario: los verbos con su significado y su idempotencia (GET, PUT y DELETE sí, POST no), las rutas que nombran recursos y no acciones, las cinco familias de códigos con la frontera decisiva entre el 4xx que arregla el cliente y el 5xx que no, y las cabeceras Content-Type, Accept y Authorization. Dominas fetch y su doble espera —la primera promesa trae las cabeceras, la segunda el cuerpo—, el objeto Response con ok, status, headers y los cinco métodos de cuerpo, y la regla de que el cuerpo se lee una sola vez salvo que lo clones. Y llevas grabada la trampa que define a esta API: fetch no rechaza con 404 ni con 500; solo rechaza si la petición no llegó a completarse. Por eso la comprobación de respuesta.ok no se recuerda, se encapsula.
Sabes enviar datos con method, headers y body —JSON.stringify obligatorio, y Content-Type prohibido cuando el cuerpo es FormData—, construir URLs con URL y URLSearchParams distinguiendo set de append, y explicar CORS: la same-origin policy, la petición simple frente al preflight OPTIONS que duplica las entradas en la pestaña Network, las cabeceras Access-Control-* que decide el servidor, y la conclusión que ahorra tardes enteras —no se arregla desde el cliente, y mode: 'no-cors' solo te da una respuesta opaca—. Y tienes clara la parte de seguridad: HTTPS siempre, tokens preferiblemente en cookie HttpOnly y no en localStorage, ninguna clave secreta en el código del cliente, y ninguna confianza en la validación del navegador.
En código, el módulo js/datos/api-tareas.js con listarTareas, obtenerTarea, crearTarea, actualizarTarea y borrarTarea, que devuelve instancias de Tarea y no objetos planos, igual que hacía RepositorioLocal: la capa datos/ es una frontera, y el resto de la aplicación no sabe si los datos vienen del disco o de la red. Y un json-server en localhost:3000 con el backlog canónico para que puedas practicar contra un servidor de verdad, porque api.tallernomada.example es y seguirá siendo ficticio.
Ahora bien: ese código funciona solo cuando todo va bien. Y en la red nada va bien todo el tiempo. El wifi del taller se cae a mitad de un POST; la API tarda quince segundos y Marta pulsa el botón tres veces; el servidor devuelve un 503 porque están desplegando; Iván escribe en el buscador y se lanzan ocho peticiones de las que llega antes la penúltima, dejando en pantalla resultados que no corresponden a lo que escribió. Ninguna de esas situaciones la cubre lo que has escrito hoy: no hay timeout, no hay cancelación, no hay reintentos, no hay estado de carga y no hay forma de decirle al usuario qué ha pasado. Eso es exactamente lo que separa una demo de una aplicación, y es el tema de Peticiones Robustas: Errores, Timeouts y AbortController, donde AbortController —presentado de pasada en 06-04— por fin ocupará el lugar que le corresponde.
Curso de JavaScript: De Principiante a Avanzado
Módulo 1: Introducción a JavaScript
- ¿Qué es JavaScript?
- Configuración de tu Entorno de Desarrollo
- Tu Primer Programa en JavaScript
- Sintaxis y Conceptos Básicos de JavaScript
- Variables y Tipos de Datos
- Operadores Básicos
- Conversión de Tipos y Comparaciones
- El Proyecto del Curso: Nómada Tareas
Módulo 2: Estructuras de Control
- Sentencias Condicionales
- Bucles: for, while, do-while
- Sentencias Switch
- Control del Flujo: break, continue y Bucles Anidados
- Manejo de Errores con try-catch
Módulo 3: Funciones
- Definición y Llamada de Funciones
- Expresiones de Función y Funciones Flecha
- Parámetros y Valores de Retorno
- Ámbito y Closures
- Hoisting y el Contexto de Ejecución
- Funciones de Orden Superior
- Recursividad
Módulo 4: Objetos y Arrays
- Introducción a los Objetos
- Métodos de Objeto y la Palabra Clave
this - Arrays: Conceptos Básicos y Métodos
- Iteración sobre Arrays
- Buscar, Ordenar y Agregar Datos: find, sort y reduce
- Desestructuración de Arrays
- Desestructuración de Objetos, Spread y Rest
- JSON y Copias de Objetos
Módulo 5: Objetos y Funciones Avanzadas
- Prototipos y Herencia
- Clases y Programación Orientada a Objetos
- Encapsulación: Getters, Setters y Campos Privados
- Módulos e Importación/Exportación
- JavaScript Asíncrono: Callbacks
- Promesas y Async/Await
- El Bucle de Eventos y la Cola de Microtareas
- Iteradores y Generadores
Módulo 6: El Modelo de Objetos del Documento (DOM)
- Introducción al DOM
- Selección y Manipulación de Elementos del DOM
- Manejo de Eventos
- Propagación, Delegación y Eventos Personalizados
- Creación y Eliminación de Elementos del DOM
- Renderizado de Listas y Plantillas HTML
- Manejo y Validación de Formularios
Módulo 7: APIs del Navegador y Temas Avanzados
- Almacenamiento Local y de Sesión
- Fetch API y AJAX
- Peticiones Robustas: Errores, Timeouts y AbortController
- WebSockets
- Service Workers y Aplicaciones Web Progresivas (PWAs)
- APIs del Navegador Esenciales
- Introducción a WebAssembly
Módulo 8: Pruebas y Depuración
- Depuración de JavaScript
- Calidad de Código: ESLint, Prettier y Convenciones
- Pruebas Unitarias con Jest
- Dobles de Prueba: Mocks, Stubs y Spies
- Pruebas de Integración
- Pruebas de Extremo a Extremo con Cypress
Módulo 9: Rendimiento y Optimización
- Medir Antes de Optimizar: DevTools y Web Vitals
- Optimización del Rendimiento de JavaScript
- Gestión de Memoria
- Manipulación Eficiente del DOM
- Carga Perezosa y División de Código
Módulo 10: Frameworks y Librerías de JavaScript
- Por Qué Existen los Frameworks
- Introducción a React
- Gestión de Estado con Redux
- Conceptos Básicos de Vue.js
- Conceptos Básicos de Angular
- Elegir el Framework Adecuado
