Native Fetch API

Modern Node.js includes the standard Fetch API out of the box. You no longer need third-party packages like axios or node-fetch to perform HTTP requests. Globals like fetch, Request, Response, Headers, and FormData work seamlessly across browser and server:

                            
const response = await fetch('https://api.github.com/users/octocat', {
    headers: { 'User-Agent': 'Node.js App' }
})

if (!response.ok) {
    throw new Error(`HTTP error: ${response.status}`)
}

const data = await response.json()
console.log(data.login, data.public_repos)
                            
                        

Built-in Test Runner

Node.js provides a built-in test runner through the node:test module and assertion utilities via node:assert/strict. You can write subtests, describe suites, mock functions, and run tests directly from the CLI with zero npm dependencies:

                            
import { test, describe, it } from 'node:test'
import assert from 'node:assert/strict'

describe('Math utilities', () => {
    it('calculates sum correctly', () => {
        const sum = (a, b) => a + b
        assert.equal(sum(2, 3), 5)
        assert.notEqual(sum(2, 3), 6)
    })

    it('handles async operations', async () => {
        const value = await Promise.resolve('ok')
        assert.deepEqual({ status: value }, { status: 'ok' })
    })
})

// Run via terminal:
// node --test
// node --test --watch
                            
                        

Native Watch Mode

Forget installing and configuring nodemon or development watchers for simple projects. Node.js has native file watching via the --watch flag, automatically restarting your process whenever imported files change:

                            
# Watch the entry point and all imported files
node --watch server.js

# Watch specific directories or pattern paths
node --watch --watch-path=./src --watch-path=./config server.js

# Clear terminal screen between restarts
node --watch --watch-preserve-output=false server.js
                            
                        

Native .env File Loading

You can load environment variables directly from files without depending on the dotenv package. Use the --env-file command line flag to automatically populate process.env:

                            
# Pass a single environment file
node --env-file=.env app.js

# Chain multiple env files (latter values override earlier ones)
node --env-file=.env --env-file=.env.local app.js

# Access variables as usual in your code
const port = process.env.PORT ?? 3000
const dbUrl = process.env.DATABASE_URL
                            
                        

Built-in SQLite Module

Node.js 22+ introduces node:sqlite, providing a built-in embedded SQLite database engine. There is no compilation step, no native bindings to maintain with node-gyp, and no third-party package needed for lightweight persistence:

                            
import { DatabaseSync } from 'node:sqlite'

const db = new DatabaseSync(':memory:')

db.exec(`
    CREATE TABLE users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT,
        role TEXT
    ) STRICT
`)

const insert = db.prepare('INSERT INTO users (name, role) VALUES (?, ?)')
insert.run('Alice', 'Admin')
insert.run('Bob', 'Developer')

const query = db.prepare('SELECT * FROM users WHERE role = ?')
const results = query.all('Admin')
console.log(results)
                            
                        

Native File Globbing

Matching filesystem patterns no longer requires external libraries such as glob or fast-glob. Node.js provides native glob support right inside the node:fs and node:fs/promises modules:

                            
import { glob } from 'node:fs/promises'

// Find all TypeScript or JavaScript test files recursively
for await (const entry of glob('src/**/*.test.{js,ts}')) {
    console.log('Found test:', entry)
}

// Synchronous version is also available
import { globSync } from 'node:fs'
const configs = globSync('**/*.config.json', { exclude: ['node_modules/**'] })
console.log(configs)
                            
                        

AbortSignal and Timeouts

Managing asynchronous timeouts and cancellations is standardized with AbortSignal. You can cancel fetch calls, event listeners, and filesystem streams automatically after a timeout or when multiple triggers race:

                            
// Automatically cancel request if it takes longer than 5 seconds
try {
    const res = await fetch('https://api.example.com/slow', {
        signal: AbortSignal.timeout(5000)
    })
    const data = await res.json()
} catch (err) {
    if (err.name === 'TimeoutError') {
        console.error('Request timed out after 5000ms')
    }
}

// Combine multiple cancellation signals with AbortSignal.any()
const userCancelled = new AbortController()
const combinedSignal = AbortSignal.any([
    userCancelled.signal,
    AbortSignal.timeout(10000)
])
                            
                        

Native WebSockets

Node.js provides a native, browser-compatible WebSocket client in the global scope. You can establish real-time connections without needing external dependencies like ws for client-side streaming:

                            
const ws = new WebSocket('wss://echo.websocket.org')

ws.addEventListener('open', () => {
    console.log('Connected to server')
    ws.send(JSON.stringify({ type: 'ping', timestamp: Date.now() }))
})

ws.addEventListener('message', (event) => {
    console.log('Received:', event.data)
})

ws.addEventListener('close', () => {
    console.log('Connection closed')
})