Pular para o conteúdo
Programação

Como usar IndexedDB no JavaScript para salvar dados offline no navegador

IndexedDB é a opção nativa do navegador para guardar dados estruturados no computador ou celular do usuário. Com ele, você consegue criar uma pequena base local em JavaScript, salvar registros, pesquisar informações e manter parte de uma aplicação funcionando mesmo quando a internet cai.

Neste guia, vou montar o exemplo de um bloco de notas offline. A ideia é entender o caminho completo: abrir o banco, criar um object store (uma espécie de tabela), gravar notas, listar registros e apagar o que não interessa. O código usa a API nativa, sem instalar biblioteca.

Diagrama que mostra aplicação JavaScript, banco IndexedDB, transação, object store e índice
O fluxo básico: o JavaScript acessa um banco associado à origem do site por meio de transações.

O que é IndexedDB e quando vale usar

IndexedDB é uma API de armazenamento local para dados estruturados. Diferentemente de uma variável JavaScript, o conteúdo continua disponível depois que a página é fechada. Diferentemente do localStorage, as operações são assíncronas e o modelo foi pensado para conjuntos maiores e registros que precisam ser consultados por chaves e índices.

O banco pertence à origem da aplicação — na prática, a combinação de protocolo, domínio e porta. Uma aplicação em https://app.exemplo.com não acessa automaticamente o banco de https://outro.exemplo.com. Essa separação é uma proteção importante, mas também significa que os dados não aparecem em outro aparelho só porque o usuário entrou com a mesma conta.

Opção Melhor uso Limitação principal
localStorage Preferências simples, como tema e idioma API síncrona e pouco adequada para registros complexos
IndexedDB Notas, filas offline, catálogos, cache e dados estruturados API mais detalhada e com transações
Servidor/API Dados que precisam sincronizar entre dispositivos Depende de rede, autenticação e infraestrutura

Há uma diferença que costuma ser esquecida: IndexedDB não é backup. O navegador pode impor limites e políticas de descarte diferentes, e o usuário pode limpar os dados do site. Para informações críticas, sincronize com um servidor e trate o armazenamento local como cache ou modo offline.

Como criar um banco IndexedDB no JavaScript

A abertura do banco recebe um nome e uma versão. Quando a versão é criada ou aumentada, o evento de atualização é o lugar correto para criar os object stores e os índices. Essa regra evita mudar a estrutura no meio de uma operação comum.

const DB_NAME = "jsms-notas";
const DB_VERSION = 1;
const STORE_NAME = "notas";

function abrirBanco() {
  return new Promise((resolve, reject) => {
    if (!("indexedDB" in window)) {
      reject(new Error("Este navegador não oferece IndexedDB."));
      return;
    }

    const request = indexedDB.open(DB_NAME, DB_VERSION);

    request.onerror = () => reject(request.error);

    request.onblocked = () => {
      reject(new Error(
        "Feche outras abas da aplicação antes de atualizar o banco."
      ));
    };

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

      if (!db.objectStoreNames.contains(STORE_NAME)) {
        const store = db.createObjectStore(STORE_NAME, {
          keyPath: "id",
          autoIncrement: true
        });

        store.createIndex("titulo", "titulo", { unique: false });
      }
    };

    request.onsuccess = () => {
      const db = request.result;

      // Permite que a aba seja fechada antes de uma mudança de versão.
      db.onversionchange = () => db.close();
      resolve(db);
    };
  });
}

O keyPath define a propriedade usada como identificador. Com autoIncrement: true, o navegador cria um número quando uma nota nova é adicionada. O índice titulo não é obrigatório para o exemplo, mas mostra como preparar buscas por uma propriedade sem depender apenas do ID.

Como salvar uma nota em uma transação

No IndexedDB, toda leitura ou gravação ocorre dentro de uma transação. Pense nela como uma caixa segura: se uma operação de escrita falhar, o navegador não deve aplicar parcialmente o conjunto de alterações.

async function salvarNota(titulo, texto) {
  const db = await abrirBanco();

  return new Promise((resolve, reject) => {
    const transaction = db.transaction(STORE_NAME, "readwrite");
    const store = transaction.objectStore(STORE_NAME);

    store.add({
      titulo: titulo.trim(),
      texto,
      atualizadoEm: new Date().toISOString()
    });

    transaction.oncomplete = () => {
      db.close();
      resolve();
    };

    transaction.onerror = () => {
      db.close();
      reject(transaction.error);
    };
  });
}

salvarNota("Comprar café", "Passar no mercado depois do trabalho")
  .then(() => console.log("Nota salva localmente"))
  .catch((erro) => console.error("Não foi possível salvar", erro));

O modo readwrite é necessário porque a função altera o banco. Para consultar sem alterar nada, use readonly. Também vale validar campos antes de abrir a transação: um banco local não substitui regras de negócio nem impede que o usuário desative o armazenamento.

Como listar, buscar e apagar registros

Para um bloco de notas pequeno, getAll() resolve a listagem. Em uma base muito grande, prefira um cursor ou um índice e carregue os resultados em partes. A API permite guardar objetos, arrays e valores suportados pelo navegador, mas ainda é responsabilidade da aplicação definir uma estrutura consistente.

async function listarNotas() {
  const db = await abrirBanco();

  return new Promise((resolve, reject) => {
    const transaction = db.transaction(STORE_NAME, "readonly");
    const request = transaction.objectStore(STORE_NAME).getAll();

    request.onsuccess = () => {
      db.close();
      resolve(request.result);
    };

    request.onerror = () => {
      db.close();
      reject(request.error);
    };
  });
}

async function apagarNota(id) {
  const db = await abrirBanco();

  return new Promise((resolve, reject) => {
    const transaction = db.transaction(STORE_NAME, "readwrite");
    transaction.objectStore(STORE_NAME).delete(id);

    transaction.oncomplete = () => {
      db.close();
      resolve();
    };

    transaction.onerror = () => {
      db.close();
      reject(transaction.error);
    };
  });
}

listarNotas().then((notas) => console.table(notas));
// Exemplo: apagarNota(1);

Para atualizar uma nota existente, use put() com o mesmo id. Se a aplicação precisar localizar registros pelo título, abra o índice criado na atualização do banco:

async function procurarPorTitulo(titulo) {
  const db = await abrirBanco();

  return new Promise((resolve, reject) => {
    const transaction = db.transaction(STORE_NAME, "readonly");
    const index = transaction
      .objectStore(STORE_NAME)
      .index("titulo");
    const request = index.getAll(titulo);

    request.onsuccess = () => {
      db.close();
      resolve(request.result);
    };

    request.onerror = () => {
      db.close();
      reject(request.error);
    };
  });
}

Como testar o exemplo no navegador

  1. Crie um arquivo HTML com um botão ou formulário e carregue o JavaScript em uma página servida por localhost ou HTTPS.
  2. Abra as ferramentas de desenvolvedor do navegador e procure a área Application ou Storage.
  3. Execute salvarNota() no fluxo do formulário e confirme se o banco jsms-notas e o object store notas apareceram.
  4. Feche e abra a página novamente. Se listarNotas() retornar o registro, a persistência local funcionou.
  5. Teste também a remoção do site nas configurações do navegador. Esse passo mostra por que dados importantes devem ser sincronizados com um servidor.

Se você quiser separar o armazenamento local do backend, o próximo passo é entender como uma aplicação conversa com uma API. O JSMS já explicou como criar uma API REST com Node.js e Express; a combinação pode servir para enviar ao servidor as notas criadas offline quando a conexão voltar.

Limitações e erros comuns

  • Não abrir o banco em HTTPS ou localhost: alguns recursos de armazenamento dependem de contexto seguro, e o comportamento muda em arquivos abertos diretamente pelo sistema.
  • Alterar o schema sem aumentar a versão: para criar ou remover stores e índices, aumente DB_VERSION e faça a mudança em onupgradeneeded.
  • Deixar abas antigas abertas: uma conexão aberta pode bloquear a mudança de versão. Fechar o banco em onversionchange ajuda a evitar esse problema.
  • Guardar segredo como se fosse cofre: qualquer script executado na origem com acesso ao banco pode ler os dados. Não use IndexedDB para esconder senha, token ou informação que exigiria proteção criptográfica.
  • Confundir offline com sincronizado: IndexedDB mantém dados naquele navegador. Para compartilhar entre dispositivos, implemente autenticação, API e uma estratégia de conflito.

Quando for útil mostrar espaço ao usuário, navigator.storage.estimate() retorna estimativas de usage e quota. Os valores não são exatos e podem variar conforme navegador, origem e política de armazenamento, então trate o resultado como indicação, não como promessa de capacidade.

Perguntas frequentes

IndexedDB funciona sem internet?

Sim. O banco local pode ser lido e alterado sem conexão, desde que a aplicação e seus arquivos já estejam disponíveis. A sincronização com um servidor é uma etapa separada.

IndexedDB substitui o localStorage?

Não em todos os casos. O localStorage continua prático para preferências pequenas; IndexedDB é mais adequado para dados estruturados, consultas e operações assíncronas.

Os dados do IndexedDB aparecem em outro computador?

Não automaticamente. O armazenamento é associado à origem e ao perfil do navegador. Para levar os dados a outro aparelho, é preciso sincronizar com um servidor ou exportá-los.

Como saber quanto espaço o banco está usando?

Use navigator.storage.estimate() para obter uma estimativa de uso e cota da origem. O retorno é aproximado e não deve ser tratado como medição exata do IndexedDB.