Almacenamiento en el Navegador
Las aplicaciones web necesitan guardar datos del lado del cliente: preferencias de usuario, carritos de compra, tokens de sesión, datos offline y más. JavaScript ofrece varias APIs para almacenar información en el navegador, cada una con características diferentes en cuanto a capacidad, persistencia, seguridad y complejidad. Elegir la correcta depende de qué tipo de datos guardás, cuánto espacio necesitás y si la información es sensible o no. Esta sección cubre localStorage, sessionStorage, cookies e IndexedDB, y te ayuda a decidir cuándo usar cada uno.
localStorage
localStorage es la forma más simple de almacenar datos en el navegador. Guarda pares clave-valor como strings que persisten indefinidamente: los datos quedan almacenados aún después de cerrar el navegador, reiniciar la computadora o pasar semanas sin visitar la página. Solo se eliminan cuando el usuario los borra manualmente (desde las herramientas de desarrollo o la configuración del navegador) o cuando tu código llama a removeItem o clear. Comparte el mismo almacenamiento entre todas las pestañas y ventanas del mismo origen (misma URL + mismo puerto + mismo protocolo).
// === CRUD basico con localStorage ===
// Guardar un valor (siempre como STRING)
localStorage.setItem("nombre", "Carlos");
localStorage.setItem("edad", "28");
localStorage.setItem("activo", "true");
// Leer un valor
const nombre = localStorage.getItem("nombre"); // "Carlos"
const edad = localStorage.getItem("edad"); // "28" (string, no number!)
const activo = localStorage.getItem("activo"); // "true" (string, no boolean!)
// Eliminar un valor especifico
localStorage.removeItem("activo");
// Eliminar TODO el almacenamiento del dominio
// localStorage.clear(); // cuidado: borra todo
// Leer un valor que no existe
const inexistente = localStorage.getItem("noExiste"); // null
// === Almacenar objetos y arrays con JSON ===
// localStorage solo guarda strings, necesitas serializar
const usuario = {
id: 1,
nombre: "Ana",
email: "[email protected]",
preferencias: { tema: "dark", idioma: "es" }
};
// Guardar: objeto -> string JSON
localStorage.setItem("usuario", JSON.stringify(usuario));
// Leer: string JSON -> objeto
const usuarioGuardado = JSON.parse(localStorage.getItem("usuario"));
console.log(usuarioGuardado.nombre); // "Ana"
console.log(usuarioGuardado.preferencias); // { tema: "dark", idioma: "es" }
// === Almacenar un array ===
const tareas = [
{ id: 1, texto: "Aprender JS", lista: true },
{ id: 2, texto: "Construir un proyecto", lista: false }
];
localStorage.setItem("tareas", JSON.stringify(tareas));
const tareasGuardadas = JSON.parse(localStorage.getItem("tareas"));
console.log(tareasGuardadas[0].texto); // "Aprender JS"
// === Verificar cuantos items hay ===
console.log(localStorage.length); // 2 ("usuario" y "tareas")
// === Obtener la clave en una posicion ===
const primeraClave = localStorage.key(0); // "nombre" (o la primera clave)
// === Recorrer todo el localStorage ===
for (let i = 0; i < localStorage.length; i++) {
const clave = localStorage.key(i);
const valor = localStorage.getItem(clave);
console.log(`${clave}: ${valor}`);
}
// === Usar for...of con Object.entries (mas moderno) ===
for (const [clave, valor] of Object.entries(localStorage)) {
console.log(`${clave}: ${valor}`);
}
Un detalle fundamental: localStorage solo almacena strings. Si guardás un número, un booleano o un objeto directamente, JavaScript lo convierte a string automáticamente y vas a tener sorpresas al leerlo. Por eso, para cualquier dato que no sea un string simple, siempre usá JSON.stringify() al guardar y JSON.parse() al leer. Si el string guardado no es JSON válido, JSON.parse() lanza un error, así que es buena práctica envolverlo en un try/catch.
// === Funcion helper segura para leer del localStorage ===
function getLocalStorage(key, fallback = null) {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : fallback;
} catch (error) {
console.error(`Error leyendo "${key}" del localStorage:`, error);
return fallback;
}
}
// Uso
const config = getLocalStorage("config", { tema: "dark" });
// Si no existe o hay error, devuelve el fallback
// === Ejemplo practico: persistir tema del sitio ===
const temaToggle = document.querySelector("#tema-toggle");
function cargarTema() {
const tema = getLocalStorage("tema", "dark");
document.documentElement.setAttribute("data-theme", tema);
}
function alternarTema() {
const actual = document.documentElement.getAttribute("data-theme");
const nuevo = actual === "dark" ? "light" : "dark";
document.documentElement.setAttribute("data-theme", nuevo);
localStorage.setItem("tema", JSON.stringify(nuevo));
}
temaToggle.addEventListener("click", alternarTema);
cargarTema(); // al cargar la pagina
// === Ejemplo: carrito de compras ===
function obtenerCarrito() {
return getLocalStorage("carrito", []);
}
function agregarAlCarrito(producto) {
const carrito = obtenerCarrito();
const existente = carrito.find(item => item.id === producto.id);
if (existente) {
existente.cantidad += 1;
} else {
carrito.push({ ...producto, cantidad: 1 });
}
localStorage.setItem("carrito", JSON.stringify(carrito));
return carrito;
}
function vaciarCarrito() {
localStorage.removeItem("carrito");
}
// === Manejar el evento "storage" (cambia en OTRA pestaña) ===
window.addEventListener("storage", (e) => {
console.log("Clave cambiada:", e.key);
console.log("Valor viejo:", e.oldValue);
console.log("Valor nuevo:", e.newValue);
console.log("URL origen:", e.url);
if (e.key === "carrito") {
actualizarInterfazCarrito();
}
});
El evento "storage" solo se dispara en otras pestañas
Cuando una pestaña modifica localStorage, el evento storage se dispara en todas las otras pestañas del mismo origen, pero no en la que hizo el cambio. Esto es útil para sincronizar estado entre pestañas (por ejemplo, si el usuario cierra sesión en una pestaña, las demás se enteran).
sessionStorage
sessionStorage es idéntico a localStorage en su API (mismos métodos: setItem, getItem, removeItem, clear, key, length) pero con una diferencia clave: los datos se eliminan automáticamente cuando la pestaña o ventana se cierra. Además, cada pestaña tiene su propia copia independiente: si abrís dos pestañas del mismo sitio, los cambios en sessionStorage de una no afectan a la otra (a diferencia de localStorage que es compartido). Incluso si recargás la pestaña, los datos se mantienen, porque la sesión de la pestaña sigue viva.
// === API identica a localStorage ===
sessionStorage.setItem("formDraft", JSON.stringify({
titulo: "Mi post",
contenido: "Escribiendo..."
}));
const draft = JSON.parse(sessionStorage.getItem("formDraft"));
console.log(draft.titulo); // "Mi post"
sessionStorage.removeItem("formDraft");
// sessionStorage.clear(); // borra todo de esta pestana
// === Caso de uso: guardar datos de un formulario temporalmente ===
const formulario = document.querySelector("#mi-formulario");
// Restaurar datos al cargar la pagina
function restaurarFormulario() {
const datos = sessionStorage.getItem("formDatos");
if (datos) {
const parsed = JSON.parse(datos);
formulario.titulo.value = parsed.titulo || "";
formulario.email.value = parsed.email || "";
formulario.mensaje.value = parsed.mensaje || "";
}
}
// Guardar datos en cada cambio
formulario.addEventListener("input", () => {
sessionStorage.setItem("formDatos", JSON.stringify({
titulo: formulario.titulo.value,
email: formulario.email.value,
mensaje: formulario.mensaje.value
}));
});
// Limpiar al enviar
formulario.addEventListener("submit", () => {
sessionStorage.removeItem("formDatos");
});
restaurarFormulario();
Los casos de uso típicos de sessionStorage incluyen: guardar el estado de un formulario mientras el usuario lo completa (para no perder datos si recarga), almacenar datos temporales de un wizard o flujo de pasos, o mantener un flag de "ya vió este popup" que debe resetearse cuando cierra la pestaña. La regla general es simple: si los datos deben sobrevivir al cierre del navegador, usá localStorage. Si solo necesitás que duren mientras la pestaña esté abierta, usá sessionStorage.
localStorage vs sessionStorage: resumen rápido
Ambos tienen la misma API, misma capacidad (~5 MB), y solo guardan strings. La diferencia es la persistencia y el alcance: localStorage persiste para siempre y se comparte entre pestañas del mismo origen. sessionStorage se borra al cerrar la pestaña y cada pestaña tiene su propio almacenamiento aislado.
Cookies
Las cookies son el mecanismo de almacenamiento más antiguo del navegador. A diferencia de localStorage, las cookies se envían automáticamente con cada petición HTTP al servidor, lo que las hace ideales para datos que el backend necesita leer (como tokens de sesión, preferencias de idioma o tracking). Tienen un límite de ~4 KB por cookie y ~50 cookies por dominio. Se pueden configurar con fecha de expiración, restricciones de ruta, dominio, seguridad (HTTPS) y flags anti-acceso desde JavaScript.
// === Leer cookies con document.cookie ===
// Devuelve TODAS las cookies como un solo string: "nombre=valor; otra=valor"
console.log(document.cookie);
// "tema=dark; idioma=es; _ga=GA1.2.123456789"
// === Escribir una cookie basica ===
document.cookie = "tema=dark";
// === Escribir una cookie con opciones ===
document.cookie = "idioma=es; path=/; max-age=31536000; SameSite=Lax";
// === Opciones de cookie explicadas ===
// expires — fecha de expiracion (formato UTC)
// La cookie se elimina automaticamente en esa fecha
document.cookie = "sesion=abc123; expires=Fri, 31 Dec 2026 23:59:59 GMT";
// max-age — segundos hasta expirar (alternativa a expires)
// max-age=86400 = 1 dia, max-age=31536000 = 1 anio
document.cookie = "token=xyz; max-age=3600"; // expira en 1 hora
// path — ruta donde la cookie es valida
// path=/ → toda la pagina
// path=/dashboard → solo bajo /dashboard
document.cookie = "panel=abierto; path=/dashboard";
// domain — dominio donde la cookie es valida
// domain=.ejemplo.com → funciona en subdominios tambien
document.cookie = "prefs=dark; domain=.ejemplo.com";
// Secure — la cookie solo se envia por HTTPS
document.cookie = "token=abc; Secure";
// HttpOnly — JavaScript NO puede leer esta cookie (solo el servidor la crea)
// NO se puede setear desde document.cookie, solo desde el backend
// document.cookie = "token=abc; HttpOnly"; // esto NO funciona desde JS
// SameSite — controla envio en peticiones cross-site
// Strict: no se envia en NINGUNA peticion cross-site (mas seguro)
// Lax: se envia en navegaciones seguras (links, GET), no en POST cross-site
// None: se envia siempre (requiere Secure)
document.cookie = "sid=xyz; SameSite=Strict; Secure";
// === Leer una cookie especifica (funcion helper) ===
function getCookie(nombre) {
const cookies = document.cookie.split("; ");
for (const cookie of cookies) {
const [clave, valor] = cookie.split("=");
if (clave === nombre) {
return decodeURIComponent(valor);
}
}
return null;
}
console.log(getCookie("tema")); // "dark"
console.log(getCookie("noExiste")); // null
// === Eliminar una cookie (setearla con max-age=0 o fecha pasada) ===
function deleteCookie(nombre, path = "/") {
document.cookie = `${nombre}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=${path}`;
}
deleteCookie("tema");
deleteCookie("token", "/dashboard"); // si tenia path especifico
// === Verificar si las cookies estan habilitadas ===
function cookiesHabilitadas() {
try {
document.cookie = "test=1";
const habilitadas = document.cookie.indexOf("test=") !== -1;
document.cookie = "test=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
return habilitadas;
} catch (e) {
return false;
}
}
Las cookies son el único mecanismo de almacenamiento que se envía al servidor automáticamente con cada request HTTP. Esto las hace necesarias para sesiones y autenticación, pero también puede ser un problema de performance si almacenás datos grandes o innecesarios en cookies, porque cada request lleva ese peso extra. Es por eso que el patrón moderno es usar cookies solo para tokens de sesión pequeños (HttpOnly, Secure, SameSite=Strict) y dejar el resto de los datos del cliente en localStorage o sessionStorage.
Seguridad: HttpOnly, Secure y SameSite
Nunca guardes datos sensibles (tokens, contraseñas, info personal) en cookies accesibles desde JavaScript. Usá HttpOnly para que solo el servidor pueda leerlas (se configuran desde el backend con Set-Cookie header). Siempre usá Secure para que solo viajen por HTTPS. Y configurá SameSite=Strict o al menos Lax para proteger contra ataques CSRF. Cookies sin estas flags son vulnerables a XSS y CSRF.
La API de cookies es horrible (y eso es normal)
La API nativa document.cookie es una de las peores de JavaScript: lee todas las cookies como un string que tenés que parsear manualmente, escribir sobreescribe todo el string, y no hay forma directa de modificar una sola cookie. En proyectos reales, muchos desarrolladores usan una librería pequeña como js-cookie para simplificar esto, o manejan las cookies exclusivamente desde el backend.
IndexedDB
IndexedDB es una base de datos NoSQL del lado del cliente. A diferencia de localStorage (que es clave-valor simple y sólo strings), IndexedDB puede almacenar objetos estructurados, archivos, blobs y datos binarios de forma eficiente. Soporta índices para búsquedas rápidas, transacciones ACID y tiene una capacidad mucho mayor (cientos de MB o incluso GB, dependiendo del navegador). Es la opción correcta cuando necesitás almacenar cantidades significativas de datos, hacer consultas complejas o trabajar offline con datos estructurados.
// === Conceptos clave de IndexedDB ===
//
// Database → la base de datos (como en cualquier DB)
// Object Store → como una "tabla" que guarda objetos
// Index → un índice para buscar rapido por una propiedad
// Transaction → operaciones atómicas (todo exita o nada)
// Cursor → iterar sobre muchos registros
//
// La API es EVENT-BASED (usa onsuccess, onerror, onupgradeneeded)
// A diferencia de localStorage que es sincrono, IndexedDB es ASINCRONO
// === Abrir (o crear) una base de datos ===
const request = indexedDB.open("MiAppDB", 1); // nombre, version
// Se ejecuta la PRIMERA vez o cuando cambia la version
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Crear un "object store" (como una tabla)
if (!db.objectStoreNames.contains("usuarios")) {
const store = db.createObjectStore("usuarios", { keyPath: "id" });
// Crear indices para buscar rapido
store.createIndex("nombre", "nombre", { unique: false });
store.createIndex("email", "email", { unique: true });
}
if (!db.objectStoreNames.contains("productos")) {
const store = db.createObjectStore("productos", { keyPath: "id", autoIncrement: true });
store.createIndex("categoria", "categoria", { unique: false });
store.createIndex("precio", "precio", { unique: false });
}
};
// === Operaciones CRUD con IndexedDB ===
function agregarUsuario(db, usuario) {
return new Promise((resolve, reject) => {
const tx = db.transaction("usuarios", "readwrite");
const store = tx.objectStore("usuarios");
const request = store.add(usuario);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
function obtenerUsuario(db, id) {
return new Promise((resolve, reject) => {
const tx = db.transaction("usuarios", "readonly");
const store = tx.objectStore("usuarios");
const request = store.get(id);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
function obtenerTodos(db) {
return new Promise((resolve, reject) => {
const tx = db.transaction("usuarios", "readonly");
const store = tx.objectStore("usuarios");
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
function eliminarUsuario(db, id) {
return new Promise((resolve, reject) => {
const tx = db.transaction("usuarios", "readwrite");
const store = tx.objectStore("usuarios");
const request = store.delete(id);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
// === Buscar por indice ===
function buscarPorNombre(db, nombre) {
return new Promise((resolve, reject) => {
const tx = db.transaction("usuarios", "readonly");
const store = tx.objectStore("usuarios");
const index = store.index("nombre");
const request = index.getAll(nombre);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// === Uso completo ===
const dbRequest = indexedDB.open("MiAppDB", 1);
dbRequest.onsuccess = async (event) => {
const db = event.target.result;
await agregarUsuario(db, { id: 1, nombre: "Ana", email: "[email protected]" });
await agregarUsuario(db, { id: 2, nombre: "Carlos", email: "[email protected]" });
const usuario = await obtenerUsuario(db, 1);
console.log(usuario); // { id: 1, nombre: "Ana", email: "[email protected]" }
const todos = await obtenerTodos(db);
console.log("Todos:", todos);
const resultados = await buscarPorNombre(db, "Ana");
console.log("Busqueda:", resultados);
};
La API de IndexedDB es notoriamente verbose y compleja (todo es callback-based y requiere manejar transacciones manualmente). Por eso, en proyectos reales casi nadie usa la API nativa directamente. En su lugar, se usan librerías wrapper como idb (de Jake Archibald, muy ligera) o Dexie.js (más completa con API promise-based). Estas librerías envuelven la API cruda en algo mucho más amigable que se parece a usar un ORM. Lo importante es entender los conceptos (object stores, índices, transacciones) aunque en la práctica uses un wrapper.
¿Cuándo usás IndexedDB?
Usá IndexedDB cuando necesitás: (1) almacenar más de 5 MB de datos, (2) guardar datos estructurados con índices y búsquedas complejas, (3) trabajar offline con sincronización posterior (PWA), (4) almacenar archivos, imágenes o blobs, o (5) necesitas transacciones ACID. Para todo lo demás, localStorage es más simple y suficiente.
¿Cuándo usar cada uno?
Elegir el mecanismo de almacenamiento correcto depende de varios factores: cuántos datos necesitás guardar, por cuánto tiempo, si el servidor necesita acceder a ellos, y qué tan sensibles son. No existe una opción "mejor" en general: cada una brilla en su caso de uso específico. La siguiente tabla resume las diferencias principales para ayudarte a decidir rápidamente.
| Caracteristica | localStorage | sessionStorage | Cookies | IndexedDB |
|-------------------|----------------|----------------|-----------------|-----------------|
| Capacidad | ~5 MB | ~5 MB | ~4 KB c/u | Cientos de MB+ |
| Tipo de dato | Solo strings | Solo strings | Solo strings | Objetos, blobs |
| Persistencia | Permanente | Hasta cerrar | Configurable | Permanente |
| Envia al servidor | No | No | Si (cada req) | No |
| Alcance | Mismo origen | Misma pestana | Mismo origen | Mismo origen |
| Sincrono/Async | Sincrono | Sincrono | Sincrono | Asincrono |
| Indices/busqueda | No | No | No | Si |
| Seguridad JS | Accesible | Accesible | HttpOnly protege| Accesible |
// === Guia rapida: que usar en cada caso ===
// ✅ localStorage: preferencias del usuario, tema, idioma, datos del carrito
localStorage.setItem("tema", JSON.stringify("dark"));
localStorage.setItem("carrito", JSON.stringify([{ id: 1, nombre: "Zapatillas" }]));
// ✅ sessionStorage: formularios incompletos, datos temporales, pasos de un wizard
sessionStorage.setItem("pasoActual", JSON.stringify(2));
sessionStorage.setItem("formDraft", JSON.stringify({ titulo: "", email: "" }));
// ✅ Cookies: tokens de sesion (HttpOnly!), preferencias que el backend necesita
// (se configuran desde el SERVIDOR con Set-Cookie header, no desde JS)
// Set-Cookie: sid=abc123; HttpOnly; Secure; SameSite=Strict; Path=/
// ✅ IndexedDB: datos offline grandes, cache de API, imagenes, historial extenso
// (normalmente con una libreria como Dexie.js o idb)
La regla de oro es empezar con la opción más simple que cubra tus necesidades. Para la mayoría de aplicaciones web, localStorage es suficiente para preferencias y datos pequeños del cliente, y cookies (configuradas desde el backend) para autenticación. Solo migrá a IndexedDB cuando realmente necesites su capacidad extra, sus índices o soporte para datos binarios. No uses IndexedDB para guardar 3 preferencias: eso sería como usar un cañón para matar una mosca.
NUNCA guardes datos sensibles en localStorage
localStorage y sessionStorage son accesibles desde cualquier JavaScript que se ejecute en tu página, incluyendo scripts de terceros (analytics, ads, extensiones del navegador). Si un atacante inyecta código XSS en tu sitio, tiene acceso completo a todo lo que guardaste allí. Para datos sensibles (tokens, contraseñas, información personal), usá cookies HttpOnly (solo configurables desde el servidor) o no las guardes del lado del cliente.
Más allá del navegador: Datos remotos
Todo lo que vimos hasta ahora es almacenamiento local: los datos viven únicamente en el navegador del usuario. Pero en la mayoría de las aplicaciones reales, los datos "de verdad" viven en un servidor o en una base de datos externa (Supabase, Firebase, una REST API propia, etc.). El puente entre tu frontend y esos datos remotos es fetch(). El patrón es siempre el mismo: pedís los datos al servidor, recibís JSON, lo convertís en objetos de JavaScript, y los mostrás en el DOM. Después podés combinar eso con cualquier mecanismo de storage local para cachear, guardar favoritos o trabajar offline.
El siguiente ejemplo usa JSONPlaceholder, una API pública y gratuita (no necesita cuenta ni API key) que devuelve datos falsos en formato JSON. Es ideal para probar y aprender. El mismo patrón aplica para cualquier API: Supabase, Firebase REST, tu propio backend, etc. — solo cambia la URL y los headers.
// === Traer datos de una API y renderizarlos en el DOM ===
// Usamos JSONPlaceholder (API publica gratuita, no necesita API key)
const contenedor = document.querySelector("#lista-usuarios");
async function cargarUsuarios() {
try {
// 1. Hacemos el pedido GET a la API
const respuesta = await fetch("https://jsonplaceholder.typicode.com/users");
// 2. Verificamos que la respuesta sea OK (status 200-299)
if (!respuesta.ok) {
throw new Error(`Error HTTP: ${respuesta.status}`);
}
// 3. Convertimos el body de la respuesta a JSON
const usuarios = await respuesta.json();
// 4. Renderizamos los datos en el DOM
contenedor.innerHTML = usuarios.map(usuario => `
<article class="usuario-card">
<h3>${usuario.name}</h3>
<p>${usuario.email}</p>
<p>${usuario.company.name}</p>
</article>
`).join("");
// 5. Opcional: guardar en localStorage como cache
localStorage.setItem("usuarios-cache", JSON.stringify({
datos: usuarios,
timestamp: Date.now()
}));
} catch (error) {
console.error("Error cargando usuarios:", error);
contenedor.innerHTML = "<p>Error al cargar los datos.</p>";
}
}
cargarUsuarios();
// === Leer desde cache si hay datos recientes (patron cache-first) ===
function obtenerUsuariosConCache(maxEdad = 5 * 60 * 1000) {
// maxEdad en ms (5 minutos por defecto)
const cache = JSON.parse(localStorage.getItem("usuarios-cache"));
if (cache && (Date.now() - cache.timestamp) < maxEdad) {
// Cache valido: usar datos locales sin hacer fetch
console.log("Datos desde cache local");
return Promise.resolve(cache.datos);
}
// Cache expirado o no existe: pedir a la API
console.log("Datos desde la API");
return fetch("https://jsonplaceholder.typicode.com/users")
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(datos => {
localStorage.setItem("usuarios-cache", JSON.stringify({
datos,
timestamp: Date.now()
}));
return datos;
});
}
// Uso
obtenerUsuariosConCache().then(usuarios => {
console.log(usuarios[0].name); // "Leanne Graham"
});
// === Enviar datos a una API (POST) ===
async function crearPost(post) {
const respuesta = await fetch("https://jsonplaceholder.typicode.com/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(post)
});
const resultado = await respuesta.json();
console.log("Post creado con ID:", resultado.id);
return resultado;
}
crearPost({
title: "Mi primer post",
body: "Contenido del post...",
userId: 1
});
El flujo completo es: fetch(URL) hace el pedido HTTP, respuesta.json() parsea el body como JSON, y después usás los datos como cualquier objeto/array de JavaScript para manipular el DOM, construir HTML dinámico, o guardar lo que necesites en storage local. El patrón de cache-first que se muestra en el ejemplo es muy común: primero revisás si tenés datos recientes en localStorage y, si no, los pedís a la API. Esto hace que tu app cargue instantáneamente en visitas repetidas y reduce las peticiones al servidor.
Para conectarte a servicios como Supabase o Firebase, el patrón es el mismo: usás fetch() contra su URL de API, pasando headers de autenticación (generalmente un token en Authorization: Bearer <token>). La diferencia es que esos servicios requieren configurar un proyecto, obtener credenciales y manejar autenticación, lo cual excede el scope de esta sección. Veremos más sobre estas herramientas en la sección de Herramientas & Workflow.
El patrón universal: fetch + JSON + DOM
No importa si usás Supabase, Firebase, tu propio backend con Node/Python/PHP, o una API pública: el mecanismo del navegador es siempre fetch(). Lo que cambia entre servicios es la URL, los headers de autenticación, y el formato exacto del JSON que envías/recibís. Aprendéste patrón bien y podés conectarte a cualquier fuente de datos desde tu frontend.
Probá en MiniDevTools
Si querés experimentar con lo que vimos en esta sección, probá el Base64 Encode/Decode o el URL Encode/Decode.