node:sqlite: a built-in SQL database with zero dependencies
Need a database for a script, a CLI, a small service, or a test fixture? On Node.js 26 you don't need to install anything — node:sqlite is a built-in, synchronous SQLite driver. No better-sqlite3, no node-gyp, no native compile step, no npm install at all. Every example below was run on a stock node:26-slim Docker image.
Opening a database
Import DatabaseSync and point it at a file — or ':memory:' for a throwaway in-memory database. The API is synchronous: no callbacks, no promises, which is exactly what you want for scripts, migrations, and tests.
import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync(':memory:'); // or './app.db' to persist
db.exec('PRAGMA foreign_keys = ON');Creating a schema
db.exec() runs one or more statements and returns nothing — perfect for DDL.
db.exec(`
CREATE TABLE authors (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL
);
CREATE TABLE posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
author_id INTEGER NOT NULL REFERENCES authors(id),
title TEXT NOT NULL,
views INTEGER NOT NULL DEFAULT 0
);
`);Inserting rows: positional and named parameters
Prepare a statement once, then .run() it with parameters. Use ? for positional binding, or $name / :name / @name for named binding with an object. .run() returns lastInsertRowid and the number of changes.
// positional (?)
const insertAuthor = db.prepare('INSERT INTO authors (name) VALUES (?)');
const a = insertAuthor.run('Alex');
a.lastInsertRowid; // 1
a.changes; // 1
// named ($name / :name / @name) with an object
const insertPost = db.prepare(
'INSERT INTO posts (author_id, title, views) VALUES ($author, $title, $views)'
);
insertPost.run({ author: a.lastInsertRowid, title: 'node:sqlite is great', views: 10 });
insertPost.run({ author: a.lastInsertRowid, title: 'Temporal in Node 26', views: 42 });Reading rows: get, all, iterate
A prepared SELECT gives three ways to read: .get() for a single row, .all() for an array, and .iterate() to stream rows one at a time without buffering the whole result set.
// one row
const one = db.prepare('SELECT * FROM posts WHERE id = ?').get(1);
// all rows (here with a JOIN)
const rows = db.prepare(`SELECT p.title, p.views, a.name AS author
FROM posts p JOIN authors a ON a.id = p.author_id
ORDER BY p.views DESC`).all();
// stream rows without buffering the whole result set
for (const r of db.prepare('SELECT title FROM posts').iterate()) {
console.log(r.title);
}Transactions
Wrap related writes in BEGIN / COMMIT and roll back on error so a failure leaves the database untouched. Here a foreign-key violation aborts the whole batch — the earlier UPDATE is undone too.
try {
db.exec('BEGIN');
db.prepare('UPDATE posts SET views = views + 1').run();
// author 999 doesn't exist -> foreign-key violation -> throws
db.prepare('INSERT INTO posts (author_id, title) VALUES (?, ?)').run(999, 'orphan');
db.exec('COMMIT');
} catch (err) {
db.exec('ROLLBACK');
console.log('rolled back:', err.code); // ERR_SQLITE_ERROR
}User-defined functions
Register a JavaScript function with db.function() and call it straight from SQL.
db.function('shout', (s) => String(s).toUpperCase() + '!');
db.prepare("SELECT shout('done') AS out").get().out; // "DONE!"Proof: the whole thing on node:26-slim
Running a script that strings all of the above together produces this — no flags, on a stock image:
$ docker run --rm node:26-slim node app.mjs
lastInsertRowid: 1 | changes: 1
get(): [Object: null prototype] {
id: 1,
author_id: 1,
title: 'node:sqlite is great',
views: 10
}
all(): [
[Object: null prototype] { title: 'Temporal in Node 26', views: 42, author: 'Alex' },
[Object: null prototype] { title: 'node:sqlite is great', views: 10, author: 'Alex' }
]
iterate(): node:sqlite is great; Temporal in Node 26;
transaction rolled back: ERR_SQLITE_ERROR
views after rollback: 10
udf: DONE!Note the rows come back as [Object: null prototype] objects — plain data bags with no inherited prototype, which is a safe default for untrusted keys.
Is it stable?
node:sqlite started out experimental but has matured: on Node 24 and 26 it runs with no flag and no warning. On Node 22 the same code still prints an experimental warning, so match your expectations to the runtime you deploy on.
# Node 22
(node:1) ExperimentalWarning: SQLite is an experimental feature and might change at any time
# Node 24 & 26 — no warningWhen to reach for it (and when not)
- Great for: CLIs, local tooling, test fixtures, caches, small single-process services, prototypes — anything you'd otherwise pull in
better-sqlite3for. - Think twice for: high-concurrency multi-process writers (SQLite locks the whole database on write), or when you need queries to never block the event loop —
node:sqliteis deliberately synchronous.