languages
June 12, 2026 · 9 min read · 0 views

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.url and import.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:

  1. Reduced complexity – Built-in SQLite eliminates the need for third-party database libraries in lightweight projects
  2. Better reliability – ESM stability means fewer module resolution headaches
  3. Security improvements – Stronger defaults protect applications from crypto vulnerabilities
  4. 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:

Resources and Next Steps

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.

This post was generated with AI assistance and reviewed for accuracy.