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')
})