Node.js 24: SQLite Integration, ESM Stability, and Production-Ready Features
Node.js 24 brings native SQLite support, improved ESM module stability, and enhanced security features. Learn what's new and how to upgrade.
Node.js 24: A Major Milestone for Backend Development
Node.js 24 represents a significant step forward for the runtime, introducing features that address long-standing developer pain points. The headline additions—native SQLite support and ESM (ECMAScript Modules) stability improvements—signal the Node.js project’s commitment to modernizing the ecosystem while maintaining backward compatibility.
Whether you’re building REST APIs, real-time applications, or microservices, Node.js 24 offers tangible improvements that can streamline development and reduce external dependencies. In this guide, we’ll explore the key features, show you how to migrate, and highlight the tools that can help you validate configurations along the way.
What’s New in Node.js 24
1. Native SQLite Support (Node.js 24.0+)
One of the most anticipated features is the addition of a built-in SQLite module. Previously, developers had to rely on third-party libraries like better-sqlite3 or sqlite3 npm packages, which added complexity, external dependencies, and potential compatibility issues across different platforms.
Node.js 24 includes a lightweight, native SQLite implementation via the sqlite module, providing:
- Zero external dependencies – SQLite is compiled directly into Node.js
- Cross-platform consistency – Same binary performance on Windows, macOS, and Linux
- Simplified database operations – Perfect for lightweight databases, caching, and embedded use cases
Using SQLite in Node.js 24
import { DatabaseSync } from 'node:sqlite';
// Create or open a database
const db = new DatabaseSync(':memory:'); // or specify a file path
// Create a table
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// Prepare a statement for repeated use
const insertUser = db.prepare(
'INSERT INTO users (name, email) VALUES (?, ?)'
);
// Insert data
insertUser.run('Alice Johnson', '[email protected]');
insertUser.run('Bob Smith', '[email protected]');
// Query data
const selectUsers = db.prepare('SELECT * FROM users WHERE email = ?');
const user = selectUsers.get('[email protected]');
console.log(user); // { id: 1, name: 'Alice Johnson', email: '[email protected]', ... }
// Iterate over results
const allUsers = db.prepare('SELECT * FROM users').all();
allUsers.forEach((user) => {
console.log(`${user.name} (${user.email})`);
});
// Update data
const updateUser = db.prepare('UPDATE users SET name = ? WHERE id = ?');
updateUser.run('Alice Smith', 1);
// Delete data
const deleteUser = db.prepare('DELETE FROM users WHERE id = ?');
deleteUser.run(1);
// Close the database
db.close();
The synchronous API is intentional—SQLite operations are typically fast enough that async overhead isn’t justified. For I/O-bound operations in high-concurrency scenarios, consider using Worker Threads to avoid blocking the event loop.
2. ESM Module Stability and Improvements
Node.js 24 marks a major milestone for ECMAScript Modules (ESM) stability. After years of gradual improvements, ESM is now the recommended module system, and Node.js 24 brings several enhancements:
- Improved require() interoperability – Better handling of CommonJS→ESM transitions
-
Enhanced import.meta support – More predictable behavior for
import.meta.urlandimport.meta.resolve() - Stable conditional exports – Package.json exports field now fully standardized
Example: Package with Conditional Exports
{
"name": "my-library",
"version": "1.0.0",
"type": "module",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
},
"./utils": {
"import": "./dist/utils.js",
"require": "./dist/utils.cjs",
"types": "./dist/utils.d.ts"
}
}
}
In Node.js 24, this configuration is more reliable across different import patterns.
3. Enhanced Security Features
Node.js 24 includes security hardening:
- Stronger cryptography defaults – OpenSSL 3.x with updated algorithms
- Web Crypto API improvements – Better support for Ed25519, EdDSA signatures
- CORS and security headers – Enhanced built-in support for web standards
If you’re working with JWT tokens or cryptographic operations, you can validate your implementations using JWT Decoder to ensure tokens are properly formatted and contain the expected claims.
Getting Started: Upgrade and Migration
Step 1: Check Your Current Node.js Version
node --version
# v22.x.x (or earlier)
Step 2: Update Node.js
Use nvm (Node Version Manager) for easy switching:
# Install nvm if you don't have it
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
# Install Node.js 24
nvm install 24
# Switch to Node.js 24
nvm use 24
# Verify
node --version
# v24.x.x
Or use the official installer from nodejs.org.
Step 3: Update Dependencies
npm update
npm audit
Step 4: Test Your Application
Run your test suite to ensure compatibility:
npm test
Step-by-Step Guide: Migrating to Native SQLite
If you’re currently using the sqlite3 or better-sqlite3 npm packages, here’s how to migrate to the native module:
Before (using better-sqlite3)
import Database from 'better-sqlite3';
const db = new Database('app.db');
const stmt = db.prepare('SELECT * FROM users WHERE id = ?');
const user = stmt.get(1);
After (using Node.js 24 native SQLite)
import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync('app.db');
const stmt = db.prepare('SELECT * FROM users WHERE id = ?');
const user = stmt.get(1);
The API is nearly identical, making migration straightforward.
Full Migration Example: Blog Application
Here’s a practical example of a simple blog backend using Node.js 24’s native SQLite:
import { DatabaseSync } from 'node:sqlite';
import { createServer } from 'node:http';
import { URL } from 'node:url';
const db = new DatabaseSync('blog.db');
// Initialize schema
db.exec(`
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
author TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS comments (
id INTEGER PRIMARY KEY,
post_id INTEGER NOT NULL,
author TEXT NOT NULL,
content TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (post_id) REFERENCES posts(id)
);
`);
const server = createServer((req, res) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const pathname = url.pathname;
const method = req.method;
// GET /posts – List all posts
if (method === 'GET' && pathname === '/posts') {
const posts = db.prepare('SELECT * FROM posts ORDER BY created_at DESC').all();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(posts));
}
// GET /posts/:id – Get single post with comments
else if (method === 'GET' && pathname.startsWith('/posts/')) {
const id = pathname.split('/')[2];
const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(id);
const comments = db.prepare('SELECT * FROM comments WHERE post_id = ? ORDER BY created_at DESC').all(id);
if (!post) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Post not found' }));
return;
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ...post, comments }));
}
// POST /posts – Create a post
else if (method === 'POST' && pathname === '/posts') {
let body = '';
req.on('data', (chunk) => { body += chunk; });
req.on('end', () => {
try {
const { title, content, author } = JSON.parse(body);
const insertPost = db.prepare(
'INSERT INTO posts (title, content, author) VALUES (?, ?, ?)'
);
const result = insertPost.run(title, content, author);
res.writeHead(201, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ id: result.lastInsertRowid, title, content, author }));
} catch (err) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid JSON' }));
}
});
}
// POST /posts/:id/comments – Add comment
else if (method === 'POST' && pathname.match(/^\/posts\/\d+\/comments$/)) {
const postId = pathname.split('/')[2];
let body = '';
req.on('data', (chunk) => { body += chunk; });
req.on('end', () => {
try {
const { author, content } = JSON.parse(body);
const insertComment = db.prepare(
'INSERT INTO comments (post_id, author, content) VALUES (?, ?, ?)'
);
const result = insertComment.run(postId, author, content);
res.writeHead(201, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ id: result.lastInsertRowid, post_id: postId, author, content }));
} catch (err) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid JSON' }));
}
});
}
else {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Not found' }));
}
});
server.listen(3000, () => {
console.log('Blog server running on http://localhost:3000');
});
This example demonstrates:
- Creating tables with foreign keys
- Inserting data and retrieving last insert ID
- Querying with parameters to prevent SQL injection
- Simple CRUD operations
Common Pitfalls and How to Avoid Them
1. Blocking the Event Loop with Synchronous SQLite
Problem: Heavy database operations can block Node.js’s event loop, stalling other requests.
Solution: Use Worker Threads for CPU-intensive or high-volume database work:
import { Worker } from 'node:worker_threads';
import { createServer } from 'node:http';
const worker = new Worker('./db-worker.js');
const server = createServer((req, res) => {
if (req.url === '/expensive-query') {
// Offload to worker thread
worker.postMessage({ query: 'SELECT COUNT(*) FROM large_table' });
worker.on('message', (result) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(result));
});
}
});
server.listen(3000);
2. SQL Injection Vulnerabilities
Problem: Concatenating user input directly into SQL queries:
// ❌ DANGEROUS
const userInput = "'; DROP TABLE users; --";
const query = `SELECT * FROM users WHERE name = '${userInput}'`;
db.exec(query); // Catastrophic!
Solution: Always use parameterized queries:
// ✅ SAFE
const stmt = db.prepare('SELECT * FROM users WHERE name = ?');
const result = stmt.get(userInput);
3. Not Closing Database Connections
Problem: Leaving connections open wastes memory and file handles:
// ❌ Connection never closed
const db = new DatabaseSync('app.db');
db.prepare('SELECT * FROM users').all();
// Process exits, database still holds a lock
Solution: Always close explicitly:
// ✅ Proper cleanup
const db = new DatabaseSync('app.db');
try {
const users = db.prepare('SELECT * FROM users').all();
console.log(users);
} finally {
db.close();
}
4. CommonJS and ESM Mixing Issues
Problem: Mixing require() and import can cause module resolution errors.
Solution: Commit to ESM in Node.js 24. Set "type": "module" in package.json:
{
"name": "my-app",
"type": "module",
"version": "1.0.0"
}
If you need to validate JSON configuration files or API responses, use JSON Formatter to ensure proper structure.
Performance Considerations
Native SQLite vs. npm Packages
| Aspect | Native SQLite | better-sqlite3 | sqlite3 |
|---|---|---|---|
| Setup time | Instant | Compile on install | Compile on install |
| Performance | Excellent | Excellent | Good |
| API | Simple, synchronous | Simple, synchronous | Callback-based |
| Dependencies | None | C++ bindings | Node-gyp |
| Platform support | All major platforms | All major platforms | All major platforms |
For most applications, the native module will provide sufficient performance while eliminating external dependency management.
Why It Matters
Node.js 24 represents maturation in several areas:
- Reduced complexity – Built-in SQLite eliminates the need for third-party database libraries in lightweight projects
- Better reliability – ESM stability means fewer module resolution headaches
- Security improvements – Stronger defaults protect applications from crypto vulnerabilities
- Developer experience – Cleaner APIs and less boilerplate code
For teams evaluating Node.js adoption or considering upgrades, Node.js 24 is a compelling milestone that addresses real pain points.
Validation and Testing Tools
When building Node.js applications, validation is critical. Here are some Kloubot tools to help:
- Use JSON Formatter to validate API response structures
- Test JWT tokens with JWT Decoder if your application handles authentication
- Generate test data with Mock Data Generator for database seeding
- Build and test HTTP endpoints with API Request Builder
- Test regular expressions for data validation with Regex Tester
Resources and Next Steps
- Official Node.js 24 Release Notes: https://nodejs.org/en/blog/release/v24.0.0/
- SQLite Module Documentation: https://nodejs.org/api/sqlite.html
- ESM Guide: https://nodejs.org/api/esm.html
- Migration Guide: https://nodejs.org/en/docs/guides/nodejs-release-working-group/
Node.js 24 is available now. Test it in a non-production environment first, then plan your upgrade. The improvements justify the minimal effort required.