</> HTML5Advent
ENFRESDEITPT

// apis

Tutorial de IndexedDB: construir una app de notas que funciona sin conexion

Un tutorial de IndexedDB paso a paso. Abrir una base de datos, crear un object store, anadir y leer registros, consultar por indice, eliminar entradas y gestionar las subidas de version, todo en una pequena app de notas que puedes ejecutar en el navegador.

Una persona de pie frente a racks de servidores con paredes de cristal en un centro de datos, sosteniendo un portatil abierto, con iluminacion azul y logos de proveedores visibles en los paneles de cristal

La guia conceptual explica que es IndexedDB y cuando usarla. Este tutorial construye una pequena aplicacion de notas desde cero para que veas cada paso: abrir una base de datos, crear un store, escribir registros, leerlos, consultar por indice y eliminar entradas.

Todo se ejecuta en el navegador. Sin herramientas de build, sin servidor, sin dependencias. Copia cada bloque en un solo archivo HTML y abrelo.

Paso 1: abrir la base de datos

Toda interaccion con IndexedDB comienza con indexedDB.open(). Le pasas un nombre y un numero de version. Si la base de datos no existe todavia, o si tu version es superior a la que hay en disco, el navegador dispara upgradeneeded.

function openDB() {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open('notes-app', 1);

    request.onupgradeneeded = (event) => {
      const db = event.target.result;
      if (!db.objectStoreNames.contains('notes')) {
        const store = db.createObjectStore('notes', {
          keyPath: 'id',
          autoIncrement: true,
        });
        store.createIndex('by_tag', 'tag', { unique: false });
        store.createIndex('by_date', 'createdAt', { unique: false });
      }
    };

    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

Algunos puntos a tener en cuenta. keyPath: 'id' le dice a IndexedDB que propiedad de cada objeto es la clave primaria. autoIncrement: true hace que la base de datos genere esa clave por ti. Las dos llamadas a createIndex te permitiran consultar notas por tag o por fecha mas adelante, sin recorrer todos los registros.

Paso 2: anadir un registro

Escribir en IndexedDB siempre ocurre dentro de una transaccion. Abres una sobre el object store que necesitas, obtienes una referencia a ese store y llamas a add() o put().

async function addNote(db, text, tag) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction('notes', 'readwrite');
    const store = tx.objectStore('notes');
    const record = {
      text,
      tag: tag || 'general',
      createdAt: new Date().toISOString(),
    };
    const request = store.add(record);
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

add() lanza un error si ya existe un registro con la misma clave. put() sobreescribe en su lugar. Para registros nuevos, add() es mas seguro porque detecta duplicados accidentales.

Paso 3: leer todos los registros

Para leer todo lo que hay en un store, abre una transaccion readonly y llama a getAll():

async function getAllNotes(db) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction('notes', 'readonly');
    const store = tx.objectStore('notes');
    const request = store.getAll();
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

Si solo necesitas un registro y conoces su clave, usa store.get(key) en su lugar. Devuelve un solo objeto en vez de un array.

Primer plano de un editor de codigo con tema oscuro en un monitor, mostrando codigo de configuracion Ruby con resaltado sintactico verde y blanco, un arbol de archivos a la izquierda y numeros de linea a la derecha
Escribir codigo en un editor oscuro. IndexedDB sigue el mismo patron de peticion y callback visible en la mayoria de APIs del navegador, envuelto aqui en promesas para mejorar la legibilidad.

Paso 4: consultar por indice

Los indices permiten encontrar registros sin recorrer todo el store. Para obtener todas las notas con el tag "work":

async function getNotesByTag(db, tag) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction('notes', 'readonly');
    const store = tx.objectStore('notes');
    const index = store.index('by_tag');
    const request = index.getAll(tag);
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

Tambien puedes usar un key range para consultar un rango de valores. Por ejemplo, para obtener notas creadas despues de una fecha determinada:

const range = IDBKeyRange.lowerBound('2026-01-01T00:00:00.000Z');
const request = store.index('by_date').getAll(range);

Los cuatro constructores de rango son lowerBound, upperBound, bound (ambos extremos) y only (coincidencia exacta). Cada uno acepta un booleano opcional para excluir el valor limite.

Paso 5: actualizar un registro

La actualizacion usa put(). Debes incluir la propiedad clave (id en nuestro esquema) para que IndexedDB sepa que registro sobreescribir:

async function updateNote(db, id, newText) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction('notes', 'readwrite');
    const store = tx.objectStore('notes');
    const getReq = store.get(id);
    getReq.onsuccess = () => {
      const note = getReq.result;
      if (!note) { reject(new Error('Not found')); return; }
      note.text = newText;
      const putReq = store.put(note);
      putReq.onsuccess = () => resolve();
      putReq.onerror = () => reject(putReq.error);
    };
    getReq.onerror = () => reject(getReq.error);
  });
}

Paso 6: eliminar un registro

Llama a store.delete(key) dentro de una transaccion readwrite:

async function deleteNote(db, id) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction('notes', 'readwrite');
    const store = tx.objectStore('notes');
    const request = store.delete(id);
    request.onsuccess = () => resolve();
    request.onerror = () => reject(request.error);
  });
}

Para borrar todos los registros de un store, usa store.clear() en su lugar.

Paso 7: gestionar subidas de version

Cuando necesitas anadir un nuevo indice o un nuevo store despues de que los usuarios ya tienen la version 1, incrementa el numero de version y gestiona la migracion en onupgradeneeded:

request.onupgradeneeded = (event) => {
  const db = event.target.result;
  const oldVersion = event.oldVersion;

  if (oldVersion < 1) {
    const store = db.createObjectStore('notes', {
      keyPath: 'id',
      autoIncrement: true,
    });
    store.createIndex('by_tag', 'tag', { unique: false });
    store.createIndex('by_date', 'createdAt', { unique: false });
  }

  if (oldVersion < 2) {
    const tx = event.target.transaction;
    const store = tx.objectStore('notes');
    store.createIndex('by_priority', 'priority', { unique: false });
  }
};

Comprueba event.oldVersion para ejecutar solo las migraciones que el usuario aun no ha visto. Un usuario que sube desde 0 (sin base de datos) ejecuta ambos bloques. Uno que sube desde 1 ejecuta solo el segundo.

Juntando todo

Aqui tienes un script minimo que ejercita todas las funciones anteriores:

(async () => {
  const db = await openDB();

  const id = await addNote(db, 'Buy groceries', 'personal');
  console.log('Added note with id:', id);

  const all = await getAllNotes(db);
  console.log('All notes:', all);

  const tagged = await getNotesByTag(db, 'personal');
  console.log('Personal notes:', tagged);

  await updateNote(db, id, 'Buy groceries and cook dinner');
  console.log('Updated note', id);

  await deleteNote(db, id);
  console.log('Deleted note', id);
})();

Abre la consola del navegador para ver la salida. Cada operacion es asincrona, cada escritura pasa por una transaccion, y los datos sobreviven a las recargas de pagina sin ningun servidor.

Errores comunes

ErrorQue ocurreSolucion
Crear un store fuera de onupgradeneededInvalidStateErrorTodos los cambios de esquema van en onupgradeneeded
Usar await dentro de un callback de transaccionLa transaccion se cierra automaticamente antes de que el await se resuelvaManten todas las operaciones del store sincronas dentro de un mismo tick de transaccion, o abre una nueva transaccion despues del await
Olvidar incrementar el numero de versiononupgradeneeded nunca se disparaIncrementa el entero de version cada vez que cambies el esquema
Pasar una cadena donde una clave espera un numeroRegistro no encontradoRespeta el tipo: si autoIncrement genera numeros, consulta con un numero

IndexedDB es verboso, pero cada pieza tiene un proposito: la version controla tu esquema, las transacciones protegen tus datos, y los indices mantienen las lecturas rapidas. Una vez que estos siete pasos se vuelvan rutina, tienes todo lo necesario para almacenar datos estructurados en el cliente, ya sea para soporte sin conexion, cache o un estado que sobreviva a la sesion.