Cuando necesitas almacenar datos estructurados en el navegador que superen los pocos kilobytes de localStorage o que requieran índices, búsquedas eficientes y transacciones, IndexedDB es la solución. Es una base de datos NoSQL orientada a objetos que vive en el navegador, persiste entre sesiones y puede almacenar prácticamente cualquier objeto JavaScript serializable.
Abrir una base de datos y crear object stores
IndexedDB es asíncrona y basada en eventos. La apertura de la base de datos devuelve una petición (IDBOpenDBRequest) con callbacks para el éxito y la creación/actualización del esquema:
function abrirDB() {
return new Promise((resolve, reject) => {
const peticion = indexedDB.open('MiAppDB', 1);
peticion.onerror = () => reject(peticion.error);
peticion.onsuccess = () => resolve(peticion.result);
// Solo se ejecuta si la versión aumenta o es la primera vez
peticion.onupgradeneeded = (e) => {
const db = e.target.result;
// Object store con clave autoincrementada
const articulos = db.createObjectStore('articulos', {
keyPath: 'id',
autoIncrement: true,
});
// Índices para búsquedas eficientes
articulos.createIndex('por_categoria', 'categoria', { unique: false });
articulos.createIndex('por_fecha', 'fecha', { unique: false });
// Object store con clave manual (string)
db.createObjectStore('configuracion', { keyPath: 'clave' });
};
});
}
CRUD básico con transacciones
Todas las operaciones en IndexedDB se realizan dentro de transacciones. Una transacción tiene un scope (los object stores que puede tocar) y un modo (readonly o readwrite):
async function guardarArticulo(db, articulo) {
return new Promise((resolve, reject) => {
const tx = db.transaction('articulos', 'readwrite');
const store = tx.objectStore('articulos');
const peticion = store.put(articulo); // put: inserta o actualiza
peticion.onsuccess = () => resolve(peticion.result); // devuelve el id
tx.onerror = () => reject(tx.error);
});
}
async function leerArticulo(db, id) {
return new Promise((resolve, reject) => {
const tx = db.transaction('articulos', 'readonly');
const peticion = tx.objectStore('articulos').get(id);
peticion.onsuccess = () => resolve(peticion.result);
peticion.onerror = () => reject(peticion.error);
});
}
async function eliminarArticulo(db, id) {
return new Promise((resolve, reject) => {
const tx = db.transaction('articulos', 'readwrite');
const peticion = tx.objectStore('articulos').delete(id);
peticion.onsuccess = () => resolve();
tx.onerror = () => reject(tx.error);
});
}
Consultas por índice con IDBIndex
Los índices permiten buscar y ordenar por campos distintos a la clave primaria:
async function articulosPorCategoria(db, categoria) {
return new Promise((resolve, reject) => {
const tx = db.transaction('articulos', 'readonly');
const indice = tx.objectStore('articulos').index('por_categoria');
const peticion = indice.getAll(categoria); // todos los que coincidan
peticion.onsuccess = () => resolve(peticion.result);
peticion.onerror = () => reject(peticion.error);
});
}
// Rango de fechas con IDBKeyRange
async function articulosEnRango(db, inicio, fin) {
return new Promise((resolve, reject) => {
const tx = db.transaction('articulos', 'readonly');
const indice = tx.objectStore('articulos').index('por_fecha');
const rango = IDBKeyRange.bound(inicio, fin);
const peticion = indice.getAll(rango);
peticion.onsuccess = () => resolve(peticion.result);
peticion.onerror = () => reject(peticion.error);
});
}
Cursor para recorrer grandes conjuntos
Para procesar muchos registros sin cargarlos todos en memoria a la vez, usa un IDBCursor:
async function procesarTodos(db, procesarFn) {
return new Promise((resolve, reject) => {
const tx = db.transaction('articulos', 'readonly');
const peticion = tx.objectStore('articulos').openCursor();
peticion.onsuccess = (e) => {
const cursor = e.target.result;
if (!cursor) { resolve(); return; } // fin del recorrido
procesarFn(cursor.value);
cursor.continue(); // avanzar al siguiente
};
peticion.onerror = () => reject(peticion.error);
});
}
La librería idb: IndexedDB con promesas
La librería idb (de Jake Archibald) envuelve IndexedDB con una API basada en promesas y async/await, eliminando toda la verbosidad de los eventos:
import { openDB } from 'idb';
const db = await openDB('MiAppDB', 1, {
upgrade(db) {
const store = db.createObjectStore('articulos', { keyPath: 'id', autoIncrement: true });
store.createIndex('categoria', 'categoria');
},
});
// CRUD limpio con async/await
await db.put('articulos', { titulo: 'Hola', categoria: 'js', fecha: new Date() });
const todos = await db.getAllFromIndex('articulos', 'categoria', 'js');
const uno = await db.get('articulos', 1);
await db.delete('articulos', 1);
// Transacción explícita
const tx = db.transaction('articulos', 'readwrite');
await tx.store.put({ titulo: 'Nuevo', categoria: 'css', fecha: new Date() });
await tx.done;
IndexedDB es la opción adecuada para datos que deben persistir entre sesiones, pueden crecer más allá de unos pocos KB, o necesitan índices y búsquedas. Para datos temporales o pequeñas preferencias, localStorage o sessionStorage siguen siendo más simples.
