O SQLite no Node.js permite armazenar dados relacionais em um único arquivo, sem executar um servidor de banco separado. Ele é útil para ferramentas locais, aplicações desktop, caches persistentes, testes, dispositivos, protótipos e serviços com volume moderado de escrita.
Versões modernas do Node.js incluem o módulo node:sqlite, que expõe classes síncronas para abrir bancos, executar SQL, preparar statements e trabalhar com transações. Como a API e sua estabilidade evoluem, é importante fixar a versão do runtime e consultar a documentação correspondente.
Neste guia, você aprenderá a abrir um banco, criar tabelas, usar parâmetros, consultar dados, executar transações, configurar WAL, aplicar migrations, lidar com concorrência, fazer backup, testar e decidir quando SQLite é adequado.
O que é SQLite?
SQLite é um banco relacional embutido. A documentação oficial do módulo SQLite no Node.js apresenta as classes e métodos disponíveis. A documentação oficial do SQLite explica SQL, locking, journaling e limites.
Para comparar com banco cliente-servidor, consulte Pool PostgreSQL no Node.js. Para ORMs e migrations, veja Prisma ORM com PostgreSQL.
Importando o módulo
const { DatabaseSync } = require('node:sqlite');Em ES Modules:
import { DatabaseSync } from 'node:sqlite';Confirme a disponibilidade na versão mínima usada pelo projeto.
Abrindo um banco
const database = new DatabaseSync('./data/app.db');Se o arquivo não existir, ele pode ser criado conforme as opções e permissões. Use caminho absoluto ou resolvido a partir do módulo.
Banco em memória
const database = new DatabaseSync(':memory:');O banco existe somente durante a vida da conexão. É útil para testes, mas não representa todos os comportamentos de filesystem e locking.
Criando uma tabela
database.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
created_at TEXT NOT NULL
)
`);exec() é apropriado para SQL estático controlado pela aplicação. Não concatene entrada do usuário.
Preparando statement
const insertUser = database.prepare(`
INSERT INTO users (email, name, created_at)
VALUES (?, ?, ?)
`);Statements preparados separam código SQL de valores e ajudam a impedir SQL injection.
Inserindo dados
const result = insertUser.run(
'ana@example.com',
'Ana',
new Date().toISOString()
);
console.log(result);O formato do resultado depende da versão, podendo incluir número de mudanças e último ID.
Parâmetros nomeados
const statement = database.prepare(`
INSERT INTO users (email, name, created_at)
VALUES ($email, $name, $createdAt)
`);
statement.run({
$email: 'leo@example.com',
$name: 'Leo',
$createdAt: new Date().toISOString()
});Use uma convenção consistente e não misture nomes de parâmetros com colunas dinâmicas.
Consultando uma linha
const findByEmail = database.prepare(`
SELECT id, email, name, created_at
FROM users
WHERE email = ?
`);
const user = findByEmail.get('ana@example.com');Quando não há resultado, o retorno pode ser undefined.
Consultando várias linhas
const listUsers = database.prepare(`
SELECT id, email, name, created_at
FROM users
ORDER BY id DESC
LIMIT ?
`);
const users = listUsers.all(20);Limite a quantidade de linhas. Carregar uma tabela inteira em memória pode bloquear o processo e consumir heap.
Iteração
Versões da API podem oferecer iteração para resultados. Consulte a documentação e prefira processamento incremental quando o conjunto for grande.
Tipos de dados
SQLite usa tipagem dinâmica com afinidades. Valores comuns incluem:
- NULL;
- INTEGER;
- REAL;
- TEXT;
- BLOB.
Datas geralmente são armazenadas como texto ISO, inteiro Unix ou Julian day. Defina uma convenção.
BigInt
Inteiros SQLite podem ultrapassar o intervalo seguro de Number. A API pode oferecer opções para retornar BigInt. Teste IDs grandes e valores monetários.
BLOB
const saveFile = database.prepare(`
INSERT INTO files (name, content)
VALUES (?, ?)
`);
saveFile.run('document.bin', buffer);Armazenar arquivos grandes no banco pode aumentar backup e locking. Em muitos sistemas, guarde o arquivo no filesystem e apenas metadados no SQLite.
Veja File System no Node.js para operações seguras.
Transações
database.exec('BEGIN');
try {
debit.run(accountA, amount);
credit.run(accountB, amount);
database.exec('COMMIT');
} catch (error) {
database.exec('ROLLBACK');
throw error;
}A transação garante atomicidade. Use finally e evite iniciar outra transação incompatível no mesmo fluxo.
BEGIN IMMEDIATE
database.exec('BEGIN IMMEDIATE');Esse modo tenta adquirir lock de escrita no início, reduzindo surpresas no meio da transação. Pode aumentar espera para outros escritores.
Prepared statements e transação
Prepare statements fora de loops quando forem reutilizados. Isso reduz parsing repetido.
Foreign keys
database.exec('PRAGMA foreign_keys = ON');Ative explicitamente por conexão quando necessário. Sem essa configuração, relacionamentos podem não ser aplicados como esperado.
WAL
database.exec('PRAGMA journal_mode = WAL');Write-Ahead Logging permite leitores durante escrita em muitos cenários e melhora concorrência. Ele cria arquivos auxiliares e exige estratégia de backup adequada.
Checkpoint
WAL acumula páginas até checkpoints. Monitore o tamanho e configure conforme a carga. Não apague arquivos -wal manualmente com o banco aberto.
Busy timeout
database.exec('PRAGMA busy_timeout = 5000');Quando o banco está bloqueado, a conexão pode aguardar por um período. Timeouts grandes escondem contenção e aumentam latência.
Concorrência
SQLite permite múltiplos leitores, mas possui limitações para escritores simultâneos. Uma API com muitas instâncias gravando no mesmo arquivo pode enfrentar database is locked.
Um processo por arquivo
Em deployments simples, manter um único processo escritor reduz contenção. Cluster e múltiplos pods compartilhando o mesmo volume exigem cuidado.
Cluster
Cada worker abre sua própria conexão. Vários escritores competem pelo lock. Veja Cluster no Node.js para processos e dimensionamento.
Network filesystems
Não presuma que todos os filesystems de rede oferecem locking compatível. Consulte recomendações do SQLite e da plataforma de armazenamento.
API síncrona e event loop
DatabaseSync executa operações síncronas. Consultas longas bloqueiam a thread JavaScript. Mantenha SQL indexado e operações curtas.
Worker Thread para banco
Uma arquitetura possível é executar SQLite em uma Worker Thread e enviar comandos por mensagens. Isso isola bloqueios da thread principal, mas exige fila, timeout e tratamento de falhas.
Veja Worker Threads no Node.js.
Índices
database.exec(`
CREATE INDEX IF NOT EXISTS idx_users_created_at
ON users(created_at)
`);Índices aceleram leitura e aumentam custo de escrita. Use EXPLAIN QUERY PLAN para validar consultas.
Paginação
OFFSET alto pode ficar lento:
SELECT * FROM users
ORDER BY id
LIMIT 20 OFFSET 100000;Prefira keyset pagination:
SELECT * FROM users
WHERE id > ?
ORDER BY id
LIMIT ?;Migrations
Mantenha uma tabela de versão:
CREATE TABLE IF NOT EXISTS schema_migrations (
version INTEGER PRIMARY KEY,
applied_at TEXT NOT NULL
);Aplique migrations em transações quando a operação permitir.
ALTER TABLE
SQLite suporta um conjunto de alterações, mas algumas mudanças exigem criar nova tabela, copiar dados e renomear. Teste migrations com cópia realista.
Backup
Copiar apenas o arquivo principal enquanto WAL está ativo pode produzir backup inconsistente. Use a API de backup suportada pela versão ou faça checkpoint e coordene a cópia.
Backup online
Versões do módulo podem oferecer função de backup. Consulte opções de páginas, progresso e cancelamento. Armazene backups em local separado.
Integridade
const result = database.prepare(
'PRAGMA integrity_check'
).all();Execute periodicamente fora do horário crítico. Uma verificação completa pode consumir recursos.
Encerramento
database.close();Feche durante shutdown para liberar locks e concluir operações. Veja Graceful Shutdown no Node.js.
Erros
Classifique:
- violação de unique;
- foreign key;
- banco bloqueado;
- arquivo sem permissão;
- SQL inválido;
- corrupção;
- disco cheio.
Não retorne SQL ou caminhos internos ao cliente.
Segurança
Use parâmetros para valores. Nomes de tabela, coluna e ORDER BY não podem ser parametrizados da mesma forma; escolha-os por allowlist.
const allowedSort = {
name: 'name',
createdAt: 'created_at'
};Permissões do arquivo
O arquivo pode conter todos os dados da aplicação. Restrinja usuário, grupo, diretório e backups. O Permission Model no Node.js pode adicionar uma camada de acesso.
Criptografia
SQLite padrão não criptografa o arquivo. Use criptografia de disco, solução compatível ou proteja dados sensíveis antes de armazenar. Gerencie chaves fora do arquivo.
Testes
Use banco temporário por teste ou arquivo isolado. Cubra:
- migrations;
- unique;
- foreign keys;
- rollback;
- concorrência;
- backup;
- disco cheio simulado;
- shutdown;
- Unicode;
- valores BigInt.
O Node Test Runner facilita setup e teardown.
Quando usar SQLite
- aplicação local;
- ferramenta CLI;
- desktop;
- testes;
- cache persistente;
- serviço com escrita moderada;
- dados por tenant em arquivos separados.
Quando usar outro banco
- muitos escritores simultâneos;
- escala horizontal com volume compartilhado;
- replicação complexa;
- alta disponibilidade gerenciada;
- grandes equipes com acesso remoto;
- consultas analíticas pesadas.
Erros comuns
- Concatenar SQL: entrada maliciosa altera a consulta.
- Esquecer foreign_keys: relações ficam inconsistentes.
- Usar em muitos pods: locking vira gargalo.
- Consulta longa na thread principal: event loop bloqueia.
- Copiar arquivo com WAL ativo: backup pode ficar incompleto.
- Não limitar resultados: memória cresce.
- Ignorar disco cheio: transações falham.
Boas práticas
- Use statements preparados.
- Ative foreign keys.
- Avalie WAL.
- Defina busy timeout curto.
- Crie índices medidos.
- Use transações.
- Faça migrations versionadas.
- Planeje backup consistente.
- Proteja o arquivo.
- Monitore latência e locks.
Conclusão
O SQLite no Node.js oferece um banco relacional simples, portátil e sem servidor separado. O módulo nativo permite preparar statements, executar transações e integrar o banco diretamente à aplicação.
O sucesso depende de respeitar suas características: escrita concorrente limitada, armazenamento em arquivo e operações síncronas. Com parâmetros, WAL, índices, migrations, backups e uma arquitetura que evita muitos escritores, SQLite pode entregar uma solução robusta para aplicações locais e serviços de escala moderada.




