Homenode:sqlite: a built-in SQL database with zero dependencies

node:sqlite: a built-in SQL database with zero dependencies

By · Node.js & JavaScript developer
Published July 5, 2026

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 warning

When 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-sqlite3 for.
  • 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:sqlite is deliberately synchronous.

Sources & further reading

About Code with Node.js

This is a personal blog and reference point of a Node.js developer.

I write and explain how different Node and JavaScript aspects work, as well as research popular and cool packages, and of course fail time to time.