- Published on
Node.js Built-in SQLite — Embedded Database Without Dependencies
- Authors

- Name
- Sanjeev Sharma
- @webcoderspeed1
Introduction
Node.js 22.5 introduced native SQLite support via the node:sqlite module. No better-sqlite3. No sql.js. No external processes. Just synchronous SQL queries backed by SQLite, built directly into Node.js.
node:sqlite Module Basics
The node:sqlite module provides DatabaseSync for synchronous database operations. Create a database file in seconds:
import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync(':memory:');
// Or persist to disk
const db = new DatabaseSync('app.db');
// Create table
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
db.close();
DatabaseSync is synchronous only; use for CLI tools, background jobs, and edge functions where blocking is acceptable.
Prepared Statements
Always use prepared statements to prevent SQL injection and improve performance.
import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync('app.db');
// Prepare statement once, execute many times
const insert = db.prepare(
'INSERT INTO users (name, email) VALUES (?, ?)'
);
const select = db.prepare(
'SELECT * FROM users WHERE id = ?'
);
const update = db.prepare(
'UPDATE users SET name = ? WHERE id = ?'
);
// Execute insert
insert.run('Alice', 'alice@example.com');
insert.run('Bob', 'bob@example.com');
// Fetch single row
const user = select.get(1);
console.log(user); // { id: 1, name: 'Alice', email: 'alice@example.com', created_at: '...' }
// Update row
update.run('Alicia', 1);
// Get all rows
const allUsers = db.prepare('SELECT * FROM users').all();
console.log(allUsers); // [{ id: 1, name: 'Alicia', ... }, { id: 2, name: 'Bob', ... }]
db.close();
Prepared statements compile once and execute efficiently, even in tight loops.
Transactions
Batch operations in transactions for atomicity and performance.
import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync('app.db');
function transferFunds(fromId: number, toId: number, amount: number) {
const selectBalance = db.prepare(
'SELECT balance FROM accounts WHERE id = ?'
);
const updateBalance = db.prepare(
'UPDATE accounts SET balance = balance + ? WHERE id = ?'
);
// Start transaction
const transaction = db.transaction(() => {
const from = selectBalance.get(fromId) as any;
if (from.balance < amount) {
throw new Error('Insufficient funds');
}
// Debit source, credit destination
updateBalance.run(-amount, fromId);
updateBalance.run(amount, toId);
return { success: true, newBalance: from.balance - amount };
});
try {
return transaction(); // Auto-commits on success
} catch (err) {
console.error('Transfer failed, rolled back:', err);
throw err;
}
}
transferFunds(1, 2, 100);
db.close();
Transactions are critical for multi-step operations; SQLite rolls back automatically on errors.
- node:sqlite Module Basics
- Prepared Statements
- Transactions
- Type Mapping (JS Numbers, Strings, BigInts)
- BLOB Handling
- WAL Mode for Performance
- Use Cases: When to Embed SQLite
- Comparing to better-sqlite3
- Limitations
- Edge Functions and Cloudflare Workers
- Checklist
- Conclusion
Type Mapping (JS Numbers, Strings, BigInts)
SQLite type system maps cleanly to JavaScript:
import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync(':memory:');
db.exec(`
CREATE TABLE products (
id INTEGER PRIMARY KEY,
name TEXT,
price REAL,
stock INTEGER,
large_id INTEGER
)
`);
const insert = db.prepare(
'INSERT INTO products (name, price, stock, large_id) VALUES (?, ?, ?, ?)'
);
// JavaScript types map directly
insert.run('Widget', 19.99, 100, 9007199254740991n); // BigInt for large integers
const select = db.prepare('SELECT * FROM products WHERE id = ?');
const product = select.get(1) as {
id: number;
name: string;
price: number;
stock: number;
large_id: bigint;
};
console.log(typeof product.price); // 'number'
console.log(typeof product.large_id); // 'bigint'
SQLite handles:
INTEGER↔numberorbigintREAL↔numberTEXT↔stringBLOB↔Uint8ArrayNULL↔null
BLOB Handling
Store binary data directly in SQLite:
import { DatabaseSync } from 'node:sqlite';
import { readFileSync } from 'fs';
const db = new DatabaseSync('app.db');
db.exec(`
CREATE TABLE files (
id INTEGER PRIMARY KEY,
name TEXT,
content BLOB,
uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
const insertFile = db.prepare(
'INSERT INTO files (name, content) VALUES (?, ?)'
);
// Store binary data
const fileContent = readFileSync('document.pdf');
insertFile.run('document.pdf', fileContent);
// Retrieve binary data
const getFile = db.prepare('SELECT * FROM files WHERE name = ?');
const file = getFile.get('document.pdf') as { content: Uint8Array };
console.log(file.content instanceof Uint8Array); // true
console.log(file.content.length); // bytes
BLOBs are returned as Uint8Array for efficient binary handling.
WAL Mode for Performance
Write-Ahead Logging (WAL) improves concurrent read/write performance significantly.
import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync('app.db');
// Enable WAL mode
db.exec('PRAGMA journal_mode = WAL');
// Optional: Tune WAL behavior
db.exec('PRAGMA synchronous = NORMAL'); // Faster, still safe
db.exec('PRAGMA cache_size = -64000'); // 64MB cache
// Queries benefit from improved concurrency
const insert = db.prepare('INSERT INTO logs (message) VALUES (?)');
for (let i = 0; i < 10000; i++) {
insert.run(`Log entry ${i}`);
}
db.close();
WAL enables multiple readers while writes are in progress, essential for concurrent workloads.
Use Cases: When to Embed SQLite
SQLite is perfect for:
- CLI tools:
netlify-cli,prisma-cliuse SQLite internally - Local-first apps: Tauri + SQLite provides offline-capable desktops
- Test databases: Eliminates dependency on external test servers
- Edge functions: Cloudflare Workers, Vercel Edge Functions (with WAasm)
- Configuration storage: Replace JSON files with queryable structure
// Example: CLI tool with local database
import { DatabaseSync } from 'node:sqlite';
import { readFileSync } from 'fs';
class TodoCLI {
private db: DatabaseSync;
constructor(dbPath: string) {
this.db = new DatabaseSync(dbPath);
this.migrate();
}
private migrate() {
this.db.exec(`
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
completed BOOLEAN DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
}
add(title: string) {
this.db.prepare('INSERT INTO todos (title) VALUES (?)').run(title);
}
list() {
return this.db.prepare('SELECT * FROM todos WHERE completed = 0').all();
}
complete(id: number) {
this.db.prepare('UPDATE todos SET completed = 1 WHERE id = ?').run(id);
}
close() {
this.db.close();
}
}
const cli = new TodoCLI('~/.todos.db');
cli.add('Deploy API');
cli.add('Write tests');
console.log(cli.list());
cli.complete(1);
cli.close();
Comparing to better-sqlite3
| Feature | node:sqlite | better-sqlite3 |
|---|---|---|
| Dependencies | 0 | Native binding, requires C++ compiler |
| Setup | Built-in | npm install, build time |
| Performance | Fast | Slightly faster (native optimizations) |
| Async | No | No (both synchronous) |
| Stability | Stable (Node 22+) | Mature (5+ years) |
| Node versions | 22.5+ only | 18+ |
| License | Node.js License | MIT |
Use node:sqlite for new projects targeting Node 22+. Use better-sqlite3 for legacy codebases or Node <22.
Limitations
- Synchronous only (no async support)
- Node.js 22.5+ required
- No built-in backup/restore
- No full-text search (but SQLite FTS5 available)
- No replication
For async database access, stay with Prisma/Drizzle backed by PostgreSQL/MySQL.
Edge Functions and Cloudflare Workers
SQLite in edge functions requires WebAssembly; use sql.js or Wasmer instead of native node:sqlite.
// Cloudflare Worker with sql.js (not node:sqlite)
import initSqlJs from 'sql.js';
export default {
async fetch(request: Request) {
const SQL = await initSqlJs();
const db = new SQL.Database();
db.run('CREATE TABLE users (id INT, name TEXT)');
db.run('INSERT INTO users VALUES (1, ?)', ['Alice']);
const results = db.exec('SELECT * FROM users');
return new Response(JSON.stringify(results), {
headers: { 'Content-Type': 'application/json' },
});
},
};
Native node:sqlite runs on Vercel Functions and other serverless Node.js runtimes where SQLite files persist.
Checklist
- Update to Node.js 22.5 LTS or later
- Replace external SQLite libraries with
node:sqlitewhere applicable - Enable WAL mode:
PRAGMA journal_mode = WAL - Always use prepared statements
- Wrap multi-step operations in transactions
- Test BLOB handling with binary files
- Configure cache size for your workload
- Monitor database file size (checkpoint periodically)
- Use TypeScript for type-safe queries
Conclusion
Node.js 22.5's built-in SQLite eliminates dependency management for CLI tools, edge functions, and local-first applications. With zero setup and zero external binaries, SQLite is now the fastest way to add persistence to Node.js projects. For backend teams building microservices, embedded SQLite accelerates development and simplifies deployment.