IndexedDB
TL;DR
IndexedDB é o banco de dados NoSQL do browser: armazena objetos JavaScript estruturados, suporta índices, transações e queries assíncronas. Cabe centenas de MB (com quotas por origem). É a solução para dados complexos que precisam persistir offline, como catálogos de produtos, dados de usuário, rascunhos, histórico. A API nativa é verbosa — em produção, use wrappers como
idbouDexie.js.
Conceitos fundamentais
Database (banco)
├── Object Store (coleção/tabela)
│ ├── Registros (objetos JS arbitrários)
│ └── Index (índice secundário para queries)
└── Transaction (agrupa operações; readonly ou readwrite)
Regras:
- Toda operação é dentro de uma transaction — nunca diretamente no banco
- Transactions com
readonlypodem rodar em paralelo;readwritesão exclusivas - Uma transaction se commita automaticamente ao terminar — ou faz rollback se houver erro
API nativa (verbosa)
// Abrir banco (cria se não existir)
const request = indexedDB.open('meu-banco', 1); // nome, versão
// Executado só quando a versão aumenta (ou banco criado pela primeira vez)
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Criar object store com keyPath (equivalente à primary key)
const store = db.createObjectStore('produtos', { keyPath: 'id' });
// Ou autoIncrement
// const store = db.createObjectStore('logs', { autoIncrement: true });
// Criar índices (para queries por campos diferentes do keyPath)
store.createIndex('categoria', 'categoria', { unique: false });
store.createIndex('nome', 'nome', { unique: false });
};
request.onsuccess = (event) => {
const db = event.target.result;
// Escrever
const tx = db.transaction('produtos', 'readwrite');
const store = tx.objectStore('produtos');
store.add({ id: 1, nome: 'Camiseta', categoria: 'roupas', preco: 49.90 });
store.put({ id: 1, nome: 'Camiseta P', categoria: 'roupas', preco: 44.90 }); // upsert
store.delete(2);
tx.oncomplete = () => console.log('Gravado com sucesso');
tx.onerror = (e) => console.error('Erro:', e.target.error);
};
request.onerror = (event) => {
console.error('Falha ao abrir banco:', event.target.error);
};Usando a biblioteca idb (recomendado)
A biblioteca idb (de Jake Archibald) envolve IndexedDB com Promises — elimina toda a verbosidade:
import { openDB } from 'idb';
const db = await openDB('meu-banco', 1, {
upgrade(db) {
const store = db.createObjectStore('produtos', { keyPath: 'id' });
store.createIndex('categoria', 'categoria');
},
});
// CRUD com async/await
await db.add('produtos', { id: 1, nome: 'Camiseta', categoria: 'roupas', preco: 49.90 });
await db.put('produtos', { id: 1, nome: 'Camiseta P', preco: 44.90 }); // upsert
await db.delete('produtos', 1);
const produto = await db.get('produtos', 1);
const todos = await db.getAll('produtos');
// Query por índice
const roupas = await db.getAllFromIndex('produtos', 'categoria', 'roupas');
// Contar
const total = await db.count('produtos');Queries com cursor
Para percorrer grandes conjuntos de dados sem carregar tudo na memória:
// Iterar com cursor (idb)
const tx = db.transaction('produtos', 'readonly');
let cursor = await tx.store.openCursor();
while (cursor) {
console.log(cursor.key, cursor.value);
cursor = await cursor.continue();
}
// Cursor com range
import { IDBKeyRange } from 'idb'; // não precisa importar — é global
// Produtos com preço entre 10 e 100
const tx2 = db.transaction('produtos', 'readonly');
const index = tx2.store.index('preco');
let cursor2 = await index.openCursor(IDBKeyRange.bound(10, 100));
while (cursor2) {
console.log(cursor2.value);
cursor2 = await cursor2.continue();
}
// Outros ranges
IDBKeyRange.only(42); // exatamente 42
IDBKeyRange.lowerBound(10); // >= 10
IDBKeyRange.upperBound(100); // <= 100
IDBKeyRange.bound(10, 100, false, true); // 10 <= x < 100 (exclusive upper)Transações explícitas
Para operações que precisam ser atômicas:
// Transferência entre stores (precisa de uma única transaction)
async function transferStock(fromId, toId, quantity) {
const tx = db.transaction('estoque', 'readwrite');
const from = await tx.store.get(fromId);
const to = await tx.store.get(toId);
if (from.quantidade < quantity) {
tx.abort(); // rollback explícito
throw new Error('Estoque insuficiente');
}
await tx.store.put({ ...from, quantidade: from.quantidade - quantity });
await tx.store.put({ ...to, quantidade: to.quantidade + quantity });
await tx.done; // aguardar commit
}Padrão: cache offline com IndexedDB
class ProductCache {
constructor(db) {
this.db = db;
}
async getOrFetch(category) {
// 1. Tentar do cache
const cached = await this.db.getAllFromIndex('products', 'category', category);
if (cached.length > 0) {
return cached;
}
// 2. Buscar da rede
try {
const response = await fetch(`/api/products?category=${category}`);
const products = await response.json();
// 3. Salvar no cache
const tx = this.db.transaction('products', 'readwrite');
await Promise.all([
...products.map(p => tx.store.put(p)),
tx.done,
]);
return products;
} catch (error) {
if (cached.length > 0) return cached; // fallback ao cache antigo
throw error;
}
}
async invalidate(category) {
const tx = this.db.transaction('products', 'readwrite');
const index = tx.store.index('category');
let cursor = await index.openCursor(IDBKeyRange.only(category));
while (cursor) {
await cursor.delete();
cursor = await cursor.continue();
}
}
}Versioning e migrações
const db = await openDB('meu-banco', 3, {
upgrade(db, oldVersion, newVersion, transaction) {
// Migrações encadeadas — cada case cai no próximo (sem break)
switch (oldVersion) {
case 0:
// banco novo
db.createObjectStore('users', { keyPath: 'id' });
// cai para case 1
case 1:
// adicionar índice que não existia na v1
transaction.objectStore('users').createIndex('email', 'email', { unique: true });
// cai para case 2
case 2:
// v2 → v3: adicionar novo store
db.createObjectStore('sessions', { keyPath: 'token' });
}
},
});Para fixar
- Qual a diferença entre
add()eput()no IndexedDB?- Por que toda operação IndexedDB precisa estar dentro de uma transaction?
- O que acontece se você não chamar
tx.doneoutx.abort()explicitamente?- Quando você usaria IndexedDB em vez de localStorage?
- Como você faria uma query de “todos os produtos da categoria ‘roupas’ com preço abaixo de R$100”?
Veja também
- 01 — Cookies e Web Storage — anterior
- 03 — Cache API e offline-first — próxima
- Workers 03 — Service Workers — Cache API integrada ao Service Worker