Node.js 24: SQLite Native Support and ESM Stability for Production
Node.js 24 brings native SQLite support and stabilizes ESM, making it easier to build production-ready backend applications without external dependencies.
Node.js 24: SQLite Native Support and ESM Stability for Production
Node.js 24 marks a significant milestone for backend JavaScript development. With native SQLite support built directly into the runtime and ESM (ECMAScript Modules) reaching production stability, developers can now build complete, dependency-light applications without reaching for external packages or worrying about module compatibility.
This release addresses long-standing pain points in the Node.js ecosystem: the need for database connectivity without heavy ORMs, and the remaining friction in module resolution for production workloads.
What’s New in Node.js 24
Native SQLite Support
Node.js 24 includes a built-in sqlite module that provides synchronous and asynchronous APIs for working with SQLite databases. This is game-changing for several reasons:
-
Zero external dependencies — No need for
better-sqlite3,sql.js, or other third-party packages - Performance — Direct C++ bindings mean faster query execution
- Simplicity — Familiar API surface with minimal learning curve
- Portability — SQLite is embedded; no separate database server needed
Here’s how simple it is to use:
import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync(':memory:');
// Create table
db.exec(`
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
)
`);
// Insert data
const stmt = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)');
stmt.run('Alice', '[email protected]');
stmt.run('Bob', '[email protected]');
// Query data
const select = db.prepare('SELECT * FROM users WHERE id = ?');
const user = select.get(1);
console.log(user); // { id: 1, name: 'Alice', email: '[email protected]' }
// Async variant
const dbAsync = new Database('app.db');
const stmtAsync = await dbAsync.prepare('SELECT * FROM users');
const allUsers = await stmtAsync.all();
The synchronous API (DatabaseSync) is ideal for simple scripts and CLI tools, while the async Database API is designed for high-concurrency server applications.
ESM Stability for Production
Node.js has been transitioning from CommonJS to ESM for years. Node.js 24 finally marks ESM as stable and production-ready.
Key improvements:
-
Consistent module resolution —
node:prefixed core modules work reliably -
Package.json
exportsfield — Better package definition and encapsulation - Top-level await — Already stable, but now fully integrated with ESM
- Conditional exports — Different code paths for browser vs. server environments
Example of modern ESM with conditional exports in a library:
{
"name": "my-lib",
"type": "module",
"exports": {
".": {
"node": "./dist/index.node.js",
"browser": "./dist/index.browser.js",
"default": "./dist/index.js"
},
"./server": {
"node": "./dist/server.js",
"default": null
}
}
}
Consumers can now reliably import:
import lib from 'my-lib'; // Resolves correctly for their environment
import { handler } from 'my-lib/server'; // Node.js only
Step-by-Step Guide: Building a Simple API with Node.js 24 + SQLite
Let’s build a minimal REST API for a task manager using only built-in Node.js features.
1. Initialize Your Project
mkdir task-api && cd task-api
npm init -y
node --version # Ensure you're on Node.js 24+
Update package.json:
{
"name": "task-api",
"type": "module",
"version": "1.0.0",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "node --watch server.js"
}
}
2. Create the Database Schema
Create db.js:
import { DatabaseSync } from 'node:sqlite';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const dbPath = path.join(__dirname, 'tasks.db');
export const db = new DatabaseSync(dbPath);
// Initialize schema
db.exec(`
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
completed BOOLEAN DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
console.log('Database initialized at', dbPath);
3. Build the API Server
Create server.js:
import { createServer } from 'node:http';
import { URL } from 'node:url';
import { db } from './db.js';
const PORT = process.env.PORT || 3000;
const server = createServer((req, res) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const method = req.method;
const pathname = url.pathname;
// CORS headers
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Content-Type', 'application/json');
if (method === 'OPTIONS') {
res.writeHead(200);
res.end();
return;
}
// GET /tasks
if (method === 'GET' && pathname === '/tasks') {
const stmt = db.prepare('SELECT * FROM tasks ORDER BY created_at DESC');
const tasks = stmt.all();
res.writeHead(200);
res.end(JSON.stringify(tasks));
return;
}
// GET /tasks/:id
if (method === 'GET' && pathname.match(/^\/tasks\/\d+$/)) {
const id = parseInt(pathname.split('/')[2]);
const stmt = db.prepare('SELECT * FROM tasks WHERE id = ?');
const task = stmt.get(id);
if (!task) {
res.writeHead(404);
res.end(JSON.stringify({ error: 'Task not found' }));
return;
}
res.writeHead(200);
res.end(JSON.stringify(task));
return;
}
// POST /tasks
if (method === 'POST' && pathname === '/tasks') {
let body = '';
req.on('data', chunk => (body += chunk.toString()));
req.on('end', () => {
try {
const { title, description } = JSON.parse(body);
if (!title) {
res.writeHead(400);
res.end(JSON.stringify({ error: 'Title is required' }));
return;
}
const stmt = db.prepare(
'INSERT INTO tasks (title, description) VALUES (?, ?)'
);
const info = stmt.run(title, description || null);
res.writeHead(201);
res.end(JSON.stringify({ id: info.lastInsertRowid, title, description }));
} catch (e) {
res.writeHead(400);
res.end(JSON.stringify({ error: 'Invalid JSON' }));
}
});
return;
}
// PUT /tasks/:id
if (method === 'PUT' && pathname.match(/^\/tasks\/\d+$/)) {
const id = parseInt(pathname.split('/')[2]);
let body = '';
req.on('data', chunk => (body += chunk.toString()));
req.on('end', () => {
try {
const updates = JSON.parse(body);
const stmt = db.prepare(
'UPDATE tasks SET title = ?, description = ?, completed = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?'
);
stmt.run(updates.title, updates.description, updates.completed, id);
res.writeHead(200);
res.end(JSON.stringify({ id, ...updates }));
} catch (e) {
res.writeHead(400);
res.end(JSON.stringify({ error: 'Invalid JSON' }));
}
});
return;
}
// DELETE /tasks/:id
if (method === 'DELETE' && pathname.match(/^\/tasks\/\d+$/)) {
const id = parseInt(pathname.split('/')[2]);
const stmt = db.prepare('DELETE FROM tasks WHERE id = ?');
stmt.run(id);
res.writeHead(204);
res.end();
return;
}
// 404
res.writeHead(404);
res.end(JSON.stringify({ error: 'Not found' }));
});
server.listen(PORT, () => {
console.log(`Task API running on http://localhost:${PORT}`);
});
4. Run the Server
npm start
Test with curl:
# Create a task
curl -X POST http://localhost:3000/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Learn Node.js 24", "description": "Explore SQLite and ESM"}'
# List tasks
curl http://localhost:3000/tasks
# Update a task
curl -X PUT http://localhost:3000/tasks/1 \
-H "Content-Type: application/json" \
-d '{"title": "Learn Node.js 24", "completed": true}'
# Delete a task
curl -X DELETE http://localhost:3000/tasks/1
Common Pitfalls and How to Avoid Them
1. Mixing CommonJS and ESM
Problem: If you try to use require() in an ESM project, Node.js will throw an error.
Solution: Stick to one module system. In package.json, set "type": "module" for ESM-only projects. If you need both, use .cjs for CommonJS files.
// ❌ This won't work in ESM
const fs = require('fs');
// ✅ This will
import fs from 'node:fs';
2. SQLite in High-Concurrency Scenarios
Problem: SQLite uses file-level locking, so it’s not ideal for thousands of concurrent connections.
Solution: Use SQLite for:
- Simple CRUD operations
- Low to medium concurrency (< 100 concurrent requests)
- Development and testing
- Edge computing and serverless
For high-concurrency production workloads, consider PostgreSQL or MySQL with a proper connection pool.
3. Blocking Operations with DatabaseSync
Problem: Using DatabaseSync in a high-traffic server can block the event loop.
Solution: Use the async Database API for server applications:
import { Database } from 'node:sqlite/async';
const db = new Database('app.db');
const stmt = await db.prepare('SELECT * FROM tasks');
const tasks = await stmt.all();
4. Not Validating JSON
Problem: Parsing untrusted JSON can crash your server if it’s malformed.
Solution: Always wrap JSON.parse() in try-catch:
try {
const data = JSON.parse(body);
// Process data
} catch (e) {
res.writeHead(400);
res.end(JSON.stringify({ error: 'Invalid JSON' }));
}
Why It Matters
Reduced Dependency Bloat
The JavaScript ecosystem has suffered from dependency explosion. A simple API might pull in 200+ transitive dependencies. Native SQLite means:
-
Faster
npm install - Smaller container images
- Fewer security vulnerabilities to track
- Easier audits with tools like Cargo Deny for Rust (similar supply chain tools are evolving for Node.js)
Better for Edge and Serverless
Functions like Cloudflare Workers, Vercel Edge Functions, and AWS Lambda benefit from:
- Smaller bundle sizes — Less code to upload
- Faster cold starts — Fewer dependencies to initialize
- Built-in persistence — SQLite files can be persisted to edge storage
Production Confidence
ESM stability means:
-
No more
ERR_MODULE_NOT_FOUNDsurprises - Cleaner package boundaries
- Better tree-shaking in bundlers
- Easier migration from other languages’ module systems
Integration with Developer Tools
When building Node.js 24 applications, you’ll likely need supporting tools:
- Validate your API responses — Use JSON Formatter to ensure your REST endpoints return valid JSON
- Debug database queries — Log and format SQL statements with SQL Formatter
- Test authentication tokens — Decode JWT tokens with JWT Decoder if you add auth to your API
- Generate test data — Use Mock Data Generator to populate your SQLite database with realistic test records
-
Manage environment variables — Store API keys and database paths securely (consider using
.envfiles withdotenvor Node.js’s built-in--env-fileflag in v20.6+)
Wrapping Up
Node.js 24 represents a maturation of the platform. By bringing SQLite and stabilizing ESM, the Node.js team has made it possible to build production-quality applications with fewer external dependencies and more confidence in module resolution.
For developers building:
- Internal tools and microservices
- Edge computing applications
- Prototypes and MVPs
- Full-stack JavaScript applications
Node.js 24 is worth upgrading to. Start with a small project, use the async Database API for any server work, and enjoy the simplicity of native SQLite.
The era of lightweight, self-contained Node.js backends is here.