6 Major Features of Node.js 20
Details of Node.js 20's new features, including experimental Permission Model and custom ESM loader hooks on a dedicated thread
Node.js major release is rolled out every six months. The new release becomes the Current release for six months, which gives library authors time to add support for them.
After six months, odd-numbered releases, such as 19, become unsupported, and even-numbered releases, such as 20, move to the Active LTS (long-term support) status and are ready for general use.
LTS release typically guarantees that critical bugs will be fixed for 30 months. Production applications should only use Active LTS or Maintenance LTS releases.
Node.js 20 was released on April 18, 2023. It becomes the Current release. It comes with six major features:
- Experimental Permission Model
- Custom ESM loader hooks on a dedicated thread
- Synchronous
import.meta.resolve() url.parse()warns URLs with ports that are not numbers- Stable test runner
- V8 JavaScript engine updated to V8 11.3
Let’s explore what they are and how to use them.
Use NVM To Explore Node
In a previous article, we provided instructions on using NVM (Node Version Manager) to manage Node.js and NPM versions.
Run the command to install node 20.0.0:
% nvm install 20.0.0
Downloading and installing node v20.0.0...
Downloading https://nodejs.org/dist/v20.0.0/node-v20.0.0-darwin-x64.tar.xz...
################################################################################################### 100.0%
Computing checksum with sha256sum
Checksums matched!
Now using node v20.0.0 (npm v9.6.4)On any window, run the command to use node 20:
% nvm use 20.0.0
Now using node v20.0.0 (npm v9.6.4)Now, we’re ready to explore:
% node --version
v20.0.0Experimental Permission Model
The Permission Model is a mechanism for restricting access to specific resources during execution. With the flag, --experimental-permission, it restricts the ability to access the file system, spawn processes, and use worker threads.
File system permission
The node:fs module enables interaction with the file system in a way modeled on standard POSIX functions. This includes the promise-based APIs in node:fs/promises and the callback and sync APIs in node:fs.
Without the flag, --experimental-permission, there is no limitation on the file system. The following readFile.js reads a file:
const fs = require('node:fs/promises');
async function readFile() {
try {
const data = await fs.readFile('/Users/jenniferfu/helloWorld.txt', {
encoding: 'utf8',
});
console.log(data);
} catch (err) {
console.log(err);
}
}
readFile();Execute node readFile.js, and it prints out the file content.
% node readFile.js
Hello world!With the flag, --experimental-permission, Node.js 20 restricts access to the file system.
% node --experimental-permission readFile.js
node:internal/modules/cjs/loader:179
const result = internalModuleStat(filename);
^
Error: Access to this API has been restricted
at stat (node:internal/modules/cjs/loader:179:18)
at Module._findPath (node:internal/modules/cjs/loader:651:16)
at resolveMainPath (node:internal/modules/run_main:15:25)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:76:24)
at node:internal/main/run_main_module:23:47 {
code: 'ERR_ACCESS_DENIED',
permission: 'FileSystemRead',
resource: '/Users/jenniferfu/node20/readFile.js'
}
Node.js v20.0.0To access the file system, special flags are required. --allow-fs-read specifies the allowed folders/files for read. --allow-fs-write specifies the allowed folders/files for write. Multiple names can be comma separated, and wildcards (*) are supported.
% node --experimental-permission --allow-fs-read=/Users/jenniferfu/ readFile.js
(node:5206) ExperimentalWarning: Permission is an experimental feature
(Use `node --trace-warnings ...` to show where the warning was created)
Hello world!In addition, Node.js 20 provides the API, process.permission.has, to check the specified permission at runtime. The following readFile.js has been appended with permission APIs:
const fs = require('node:fs/promises');
async function readFile() {
try {
const data = await fs.readFile('/Users/jenniferfu/helloWorld.txt', {
encoding: 'utf8',
});
console.log(data);
} catch (err) {
console.log(err);
}
}
readFile();
console.log('General read permission', process.permission.has('fs.read'));
console.log(
'/Users/jenniferfu folder read permission',
process.permission.has('fs.read', '/Users/jenniferfu')
);
console.log('General write permission', process.permission.has('fs.write'));
console.log(
'/Users/jenniferfu folder write permission',
process.permission.has('fs.write', '/Users/jenniferfu/helloWorld.txt')
);Execute node — experimental-permission — allow-fs-read=/Users/jenniferfu/ — allow-fs-write=/Users/jenniferfu/ readFile.js, and it works with specified read and write permissions.
% node --experimental-permission --allow-fs-read=/Users/jenniferfu/ --allow-fs-write=/Users/jenniferfu/ readFile.js
General read permission false
/Users/jenniferfu folder read permission true
General write permission false
/Users/jenniferfu folder write permission true
(node:6334) ExperimentalWarning: Permission is an experimental feature
(Use `node --trace-warnings ...` to show where the warning was created)Spawn process permission
The node:child_process module provides the ability to spawn subprocesses. Without the flag, --experimental-permission, there is no limitation on the spawning process. The following spawnChild.js spawns a process to perform ls /Users/jenniferfu/node20.
const { spawn } = require('node:child_process');
const ls = spawn('ls', ['/Users/jenniferfu/node20']);
ls.stdout.on('data', (data) => {
console.log(`stdout:\n${data}`);
});
ls.stderr.on('data', (err) => {
console.error(`stderr: ${err}`);
});
ls.on('close', (code) => {
console.log(`child process exited with code ${code}`);
});Execute node spawnChild.js, and it prints out the two file names in /Users/jenniferfu/node20, along with the process exiting message.
% node spawnChild.js
stdout:
readFile.js
spawnChild.js
child process exited with code 0With the flag, --experimental-permission, Node.js 20 restricts spawn processes.
% node --experimental-permission spawnChild.js
node:internal/modules/cjs/loader:179
const result = internalModuleStat(filename);
^
Error: Access to this API has been restricted
at stat (node:internal/modules/cjs/loader:179:18)
at Module._findPath (node:internal/modules/cjs/loader:651:16)
at resolveMainPath (node:internal/modules/run_main:15:25)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:76:24)
at node:internal/main/run_main_module:23:47 {
code: 'ERR_ACCESS_DENIED',
permission: 'FileSystemRead',
resource: '/Users/jenniferfu/node20/spawnChild.js'
}
Node.js v20.0.0To spawn a child process, a special flag is required. It needs --allow-child-process to allow the use of child processes. In addition, it also needs the flag to read the file folder, --allow-fs-read=/Users/jenniferfu/.
% node --experimental-permission --allow-child-process --allow-fs-read=/Users/jenniferfu/ spawnChild.js
(node:7345) ExperimentalWarning: Permission is an experimental feature
(Use `node --trace-warnings ...` to show where the warning was created)
(node:7345) SecurityWarning: The flag --allow-child-process must be used with extreme caution. It could invalidate the permission model.
stdout:
readFile.js
spawnChild.js
child process exited with code 0Worker thread permission
The node:worker_threads module enables the use of threads that execute JavaScript in parallel. Workers (threads) are useful for performing CPU-intensive JavaScript operations, but they do not help much with I/O-intensive work.
Without the flag, --experimental-permission, there is no limitation on the worker thread. The following workerThreads.js has the main thread and two workers:
const { Worker, isMainThread, workerData } = require('node:worker_threads');
if (isMainThread) {
// _filename re-loads the current file inside a Worker instance
new Worker(__filename, { workerData: 1});
new Worker(__filename, { workerData: 2});
console.log(`Inside Main Thread: isMainThread = ${isMainThread}`);
} else {
console.log(`Inside Worker: isMainThread = ${isMainThread}, and my ID is ${workerData}`);
} Execute node workerThreads.js, and it shows the main thread, the worker with ID 1, and another worker with ID 2.
% node workerThreads.js
Inside Main Thread: isMainThread = true
Inside Worker: isMainThread = false, and my ID is 1
Inside Worker: isMainThread = false, and my ID is 2With the flag, --experimental-permission, Node.js 20 restricts us to worker threads.
% node --experimental-permission workerThreads.js
node:internal/modules/cjs/loader:179
const result = internalModuleStat(filename);
^
Error: Access to this API has been restricted
at stat (node:internal/modules/cjs/loader:179:18)
at Module._findPath (node:internal/modules/cjs/loader:651:16)
at resolveMainPath (node:internal/modules/run_main:15:25)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:76:24)
at node:internal/main/run_main_module:23:47 {
code: 'ERR_ACCESS_DENIED',
permission: 'FileSystemRead',
resource: '/Users/jenniferfu/node20/workerThreads.js'
}
Node.js v20.0.0To use worker threads, a special flag is required. It needs --allow-worker to allow the use of workers. In addition, it also needs the flag to read the file itself, --allow-fs-read=/Users/jenniferfu/node20/workerThreads.js.
% node --experimental-permission --allow-worker --allow-fs-read=/Users/jenniferfu/node20/workerThreads.js workerThreads.js
Inside Main Thread: isMainThread = true
(node:8655) ExperimentalWarning: Permission is an experimental feature
(Use `node --trace-warnings ...` to show where the warning was created)
(node:8655) SecurityWarning: The flag --allow-worker must be used with extreme caution. It could invalidate the permission model.
(node:8655) ExperimentalWarning: Permission is an experimental feature
(Use `node --trace-warnings ...` to show where the warning was created)
(node:8655) ExperimentalWarning: Permission is an experimental feature
(Use `node --trace-warnings ...` to show where the warning was created)
Inside Worker: isMainThread = false, and my ID is 1
Inside Worker: isMainThread = false, and my ID is 2
(node:8655) SecurityWarning: The flag --allow-worker must be used with extreme caution. It could invalidate the permission model.
(node:8655) SecurityWarning: The flag --allow-worker must be used with extreme caution. It could invalidate the permission model.Custom ESM Loader Hooks on a Dedicated Thread
ESM hooks supplied via loaders (--experimental-loader=myLoader.mjs) now run in a dedicated thread, isolated from the main thread. This provides a separate scope for loaders and ensures no cross-contamination between loaders and application code.
As we discussed in another article, files ending with .mjs are treated as ES Modules, which use import instead of require.
Here is readFile.mjs:
import fs from 'node:fs/promises';
async function readFile() {
try {
const data = await fs.readFile('/Users/jenniferfu/helloWorld.txt', {
encoding: 'utf8',
});
console.log(data);
} catch (err) {
console.log(err);
}
}
readFile();We create an empty file, myLoader.mjs, which is passed in as an ESM loader.
% node --experimental-loader=./myLoader.mjs readFile.mjs
(node:9201) ExperimentalWarning: Custom ESM Loaders is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
Hello world!It works, although the empty file is very artificial.
Here is a rudimentary HTTPS loader, https-loader.mjs, provided by Node.js 20’s documentation:
import { get } from 'node:https';
export function resolve(specifier, context, nextResolve) {
const { parentURL = null } = context;
// Normally Node.js would error on specifiers starting with 'https://', so
// this hook intercepts them and converts them into absolute URLs to be
// passed along to the later hooks below.
if (specifier.startsWith('https://')) {
return {
shortCircuit: true,
url: specifier,
};
} else if (parentURL && parentURL.startsWith('https://')) {
return {
shortCircuit: true,
url: new URL(specifier, parentURL).href,
};
}
// Let Node.js handle all other specifiers.
return nextResolve(specifier);
}
export function load(url, context, nextLoad) {
// For JavaScript to be loaded over the network, we need to fetch and
// return it.
if (url.startsWith('https://')) {
return new Promise((resolve, reject) => {
get(url, (res) => {
let data = '';
res.on('data', (chunk) => data += chunk);
res.on('end', () => resolve({
// This example assumes all network-provided JavaScript is ES Module
// code.
format: 'module',
shortCircuit: true,
source: data,
}));
}).on('error', (err) => reject(err));
});
}
// Let Node.js handle all other URLs.
return nextLoad(url);
} It works too.
% node --experimental-loader=./https-loader.mjs readFile.mjs
(node:9260) ExperimentalWarning: Custom ESM Loaders is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
Hello world!Synchronous import.meta.resolve()
The import.meta meta-property exposes context-specific metadata to a JavaScript module. import.meta has two values, import.meta.url and import.meta.resolve().
Example for import.meta.url
import.meta.url is the full URL to the module, including query parameters and/or hash (following the ? or #).
Here is index2.mjs, which searches for the query, someURLInfo, from the specified URL.
console.log(new URL(import.meta.url).searchParams.get('someURLInfo'));Here is index.mjs, which imports the module, index2.mjs, with the query, someURLInfo=5.
import './index2.mjs?someURLInfo=5';Execute node index.mjs, and we get the value of someURLInfo.
% node index.mjs
5Example for import.meta.resolve()
import.meta.resolve() resolves a module specifier to a URL using the current module’s URL as a base.
The following resolveExample.mjs calls import.meta.resolve() for a specific URL:
const filePath = import.meta.resolve('./index.mjs?someURLInfo=5');
console.log('filePath', filePath); // file:///Users/jenniferfu/node20/index.mjs?someURLInfo=5
console.log(await import(filePath)); // 5Execute node — experimental-import-meta-resolve resolveExample.mjs, and it prints out filePath and the query value of someURLInfo.
% node --experimental-import-meta-resolve resolveExample.mjs
filePath file:///Users/jenniferfu/node20/index.mjs?someURLInfo=5
5
[Module: null prototype] { }Since import.meta.resolve() is an experimental feature, the flag, --experimental-import-meta-resolve, is needed for ES Module import.meta.resolve() support.
In alignment with browser behavior, import.meta.resolve() returns synchronously in Node.js 20. Despite this, user loader resolve hooks can still be defined as async functions (or as sync functions). Even when there are async resolve() hooks loaded, import.meta.resolve() still returns synchronously for application code.
url.parse() Warns URLs With Ports That Are Not Numbers
url.parse() accepts URLs with ports that are not numbers. This behavior might result in hostname spoofing with unexpected input. These URLs will throw an error in future versions of Node.js, as the WHATWG URL API already does. Starting with Node.js 20, these URLS cause url.parse() to emit a warning.
Here is urlParse.js:
const url = require('node:url');
url.parse('https://example.com:80/some/path?pageNumber=5'); // no warning
url.parse('https://example.com:abc/some/path?pageNumber=5'); // show warningExecute node urlParse.js . https://example.com:80/some/path?pageNumber=5 with a numerical port does not show a warning, but https://example.com:abc/some/path?pageNumber=5 with a string port shows a warning.
% node urlParse.js
(node:21534) [DEP0170] DeprecationWarning: The URL https://example.com:abc/some/path?pageNumber=5 is invalid. Future versions of Node.js will throw an error.
(Use `node --trace-deprecation ...` to show where the warning was created)Stable Test Runner
Node.js 20 includes an important change to the test_runner module. The module has been marked as stable after a recent update. Previously, the test_runner module was experimental, but this change marks it as a stable module ready for production use.
The test runner examples are listed in Node.js 18 article.
V8 JavaScript Engine Updated to V8 11.3
Node.js 20 has updated the V8 JavaScript engine to V8 11.3, which includes several new features. Let’s take a look at them.
String.prototype.isWellFormed() and toWellFormed()
JavaScript uses UTF-16 to encode its strings. This means two bytes (16-bit) represent one Unicode Code Point, which is called surrogate pairs. If a string contains any lone surrogates, it is not well formed. '\uD83D\uDE00' shows a happy face 😀. '\uD83D\' is a lone high surrogate, and '\uDE00\' is a lone low surrogate.
String.prototype.isWellFormed() can detect whether the string is well formed. both '\uD83D\' and '\uDE00\' are not well formed.
String.prototype.toWellFormed() returns a string where all lone surrogates of this string are replaced with the Unicode replacement character � ('\uFFFD').
Here is strings.js, which calls isWellFormed() and toWellFormed().
console.log('\uD83D\uDE00'); // 😀
console.log('\uFFFD'); // �
console.log('\uD83D\uDE00'.isWellFormed()); // true
console.log('\uD83D\uDE00'.toWellFormed()); // 😀
console.log('\uFFFD'.isWellFormed()); // true
console.log('\uFFFD'.toWellFormed()); // �
console.log('\uD83D'.isWellFormed()); // false
console.log('\uD83D'.toWellFormed()); // �
console.log('\uDE00'.isWellFormed()); // false
console.log('\uDE00'.toWellFormed()); // �Execute node strings.js, and here is the output:
% node strings.js
😀
�
true
😀
true
�
false
�
false
�Methods that change Array and TypedArray by copy
Existing Array.prototype and TypedArray.prototype methods modify the receiver in place. New methods are introduced in JavaScript engine V8 11.3 to provide alternatives that return modified copies of the receiver array, leaving the original unchanged. These new methods encourage the use of immutable data.
Array.prototype.reverse()reverses an array in place and returns the reference to the same array — the first array element becomes the last, and the last array element becomes the first.Array.prototype.toReversed()is the copying version of thereverse()method. It returns a new array with the elements in reversed order.Array.prototype.sort()sorts the elements of an array in place and returns the reference to the same array, now sorted.Array.prototype.toSorted()is the copying version of thesort()method. It returns a new array with the elements sorted in ascending order.Array.prototype.splice()changes the contents of an array by removing or replacing existing elements and/or adding new elements in place.Array.prototype.toSpliced()is the copying version of thesplice()method. It returns a new array with some elements removed and/or replaced at a given index.Array.prototype.with()is the copying version of using the bracket notation to change the value of a given index. It returns a new array with the element at the given index replaced with the given value.
Here is arrayMethods.js, which shows some examples extracted from this article.
const arr = [1, 2, 3, 6, 5, 4];
console.log(arr.toReversed()); // [4, 5, 6, 3, 2, 1]
console.log(arr.toSorted()); // [1, 2, 3, 4, 5, 6]
console.log(arr.toSpliced(1, 2, 7, 8, 9)); // [1, 7, 8, 9, 6, 5, 4]
console.log(arr.with(0, 9)); // [9, 2, 3, 6, 5, 4]
console.log(arr.with(1, 9)); // [1, 9, 3, 6, 5, 4]
console.log(arr); // [1, 2, 3, 6, 5, 4]
console.log(arr.sort()); // [1, 2, 3, 4, 5, 6]
console.log(arr); // [1, 2, 3, 4, 5, 6]
console.log(arr.reverse()); // [ 6, 5, 4, 3, 2, 1 ]
console.log(arr); // [ 6, 5, 4, 3, 2, 1 ]
console.log(arr.splice(1, 2, 7, 8, 9)); // [ 5, 4 ]
console.log(arr); // [ 6, 7, 8, 9, 3, 2, 1]
arr[0] = 9;
console.log(arr); // [ 9, 7, 8, 9, 3, 2, 1]
arr[1] = 9;
console.log(arr); // [ 9, 9, 8, 9, 3, 2, 1]Execute node arrayMethods.js, and we see the differences between the methods that mutate an array and those that do not.
% node arrayMethods.js
[ 4, 5, 6, 3, 2, 1 ]
[ 1, 2, 3, 4, 5, 6 ]
[
1, 7, 8, 9,
6, 5, 4
]
[ 9, 2, 3, 6, 5, 4 ]
[ 1, 9, 3, 6, 5, 4 ]
[ 1, 2, 3, 6, 5, 4 ]
[ 1, 2, 3, 4, 5, 6 ]
[ 1, 2, 3, 4, 5, 6 ]
[ 6, 5, 4, 3, 2, 1 ]
[ 6, 5, 4, 3, 2, 1 ]
[ 5, 4 ]
[
6, 7, 8, 9,
3, 2, 1
]
[
9, 7, 8, 9,
3, 2, 1
]
[
9, 9, 8, 9,
3, 2, 1
]Similar new methods are introduced in JavaScript engine V8 11.3 for TypedArray, which are Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array, BigInt64Array, and BigUint64Array.
Resizable ArrayBuffer and growable SharedArrayBuffer
Starting from JavaScript engine V8 11.3, ArrayBuffer is resizable if maxByteLength is set.
Here is arrayBufferResizable.js:
const buffer1 = new ArrayBuffer(8, { maxByteLength: 16 } );
const buffer2 = new ArrayBuffer(8);
console.log(buffer1.resizable); // true
buffer1.resize(12);
console.log(buffer2.resizable); // false
buffer2.resize(12); // throw an exceptionExecute node arrayBufferResizable.js. It shows that buffer1 is resizable, but buffer2 is not.
% node arrayBufferResizable.js
true
false
/Users/jenniferfu/node20/arrayBufferResizable.js:7
buffer2.resize(12);
^
TypeError: Method ArrayBuffer.prototype.resize called on incompatible receiver #<ArrayBuffer>
at ArrayBuffer.resize (<anonymous>)
at Object.<anonymous> (/Users/jenniferfu/node20/arrayBufferResizable.js:7:9)
at Module._compile (node:internal/modules/cjs/loader:1267:14)
at Module._extensions..js (node:internal/modules/cjs/loader:1321:10)
at Module.load (node:internal/modules/cjs/loader:1125:32)
at Module._load (node:internal/modules/cjs/loader:965:12)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:83:12)
at node:internal/main/run_main_module:23:47
Node.js v20.0.0Starting from JavaScript engine V8 11.3, SharedArrayBuffer is growable if maxByteLength is set.
Here is sharedArrayBufferGrowable.js:
const buffer1 = new SharedArrayBuffer(8, { maxByteLength: 16 } );
const buffer2 = new SharedArrayBuffer(8);
console.log(buffer1.growable); // true
buffer1.grow(12);
console.log(buffer2.growable); // false
buffer2.grow(12); // throw exceptionExecute node sharedArrayBufferGrowable.js. It shows that buffer1 is growable, but buffer2 is not.
% node sharedArrayBufferGrowable.js
true
false
/Users/jenniferfu/node20/sharedArrayBufferGrowable.js:7
buffer2.grow(12); // throw exception
^
TypeError: Method SharedArrayBuffer.prototype.grow called on incompatible receiver #<SharedArrayBuffer>
at SharedArrayBuffer.grow (<anonymous>)
at Object.<anonymous> (/Users/jenniferfu/node20/sharedArrayBufferGrowable.js:7:9)
at Module._compile (node:internal/modules/cjs/loader:1267:14)
at Module._extensions..js (node:internal/modules/cjs/loader:1321:10)
at Module.load (node:internal/modules/cjs/loader:1125:32)
at Module._load (node:internal/modules/cjs/loader:965:12)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:83:12)
at node:internal/main/run_main_module:23:47
Node.js v20.0.0RegExp v flag with set notation + properties of strings
Regular expressions have support of the u flag, such as /…/u, for Unicode-aware regular expressions. The v flag, such as /…/v, enables all the good parts of the u flag, but with additional features and improvements. The u and v flags cannot be combined in a regular expression. However, individually, they can be combined with other flags.
Here is uvRegex.js:
const re1 = /^\p{Emoji}$/u;
// Match an emoji that consists of just 1 code point:
console.log(re1.test('⚽')); // '\u26BD' true
// Match an emoji that consists of multiple code points:
console.log(re1.test('👨🏾⚕️')); // '\u{1F468}\u{1F3FE}\u200D\u2695\uFE0F' false
const re2 = /^\p{RGI_Emoji}$/v;
// Match an emoji that consists of just 1 code point:
console.log(re2.test('⚽')); // '\u26BD' true
// Match an emoji that consists of multiple code points:
console.log(re2.test('👨🏾⚕️')); // '\u{1F468}\u{1F3FE}\u200D\u2695\uFE0F' trueExecute node uvRegex.js. It shows that re1 with the u flag cannot match an emoji that consists of multiple code points, but re2 with the v flag can match an emoji that consists of multiple code points.
% node uvRegex.js
true
false
true
trueThe v flag enables support for the following Unicode properties of strings from the get-go:
Basic_EmojiEmoji_Keycap_SequenceRGI_Emoji_Modifier_SequenceRGI_Emoji_Flag_SequenceRGI_Emoji_Tag_SequenceRGI_Emoji_ZWJ_SequenceRGI_Emoji
Conclusion
Node.js 20 has several new features and improvements, including experimental Permission Model, custom ESM loader hooks on a dedicated thread, and V8 JavaScript engine 11.3 features. It is the Current release until node.js 21 is released.
If you want to check out features of other releases, take a look at the following articles:
- 7 Major Features of Node.js 21
- 6 Major Features of Node.js 19
- 5 Major Features of Node.js 18
- 3 Major Features of Node.js 17
- A Quick Look at the Node.js 16 Features
- What’s New in Node.js 15
Here is a list of npm features:
- 5 Features in npm 10
- Exploring New Features in npm 9
- A Quick Glance at npm 8 Features and Predictions for npm 9 — A Close Look at ES Modules (ESM)
- The Step-by-Step Guide to Understanding and Adopting npm 7
Thanks for reading.
Want to Connect?
If you are interested, check out my directory of web development articles.





