The Node.js team announced the release of version 18 on April 19. Node.js 18 has some significant new features that Node developers should be aware of, including:
- The upgrade of the V8 engine to version 10.1.
- An experimental test runner module.
- Most excitingly, experimental support for browser APIs like
fetch
and Streams.
In this article, we'll look at some of the major highlights from this release.
Long-term Support Release Status for Node.js 18
Node.js 18 is a long-term support (LTS) release, so it will continue to receive updates for 30 months. Because this is a new release, it is considered the current release, meaning development work on this version is ongoing, and we should expect regular updates.
After six months, in October 2022, it will become the "active LTS" release, because version 19 will be released. The active LTS release will continue to receive updates, which may be either bug fixes, security patches, or new features backported from the current release.
Once an LTS release switches to maintenance mode, typically at least two years after release, it will receive security patches and bugfixes almost exclusively.
V8 Engine Upgrade in Node.js 18
The V8 engine is the JavaScript runtime developed by Google for Chrome, and used by Node.js to execute JavaScript. Any changes to how we use the JavaScript language inside Node.js must come through the V8 engine.
V8's upgrade to 10.1 means that the Array methods findLast
and findLastIndex
are now available in Node.js. These methods return the last item or index of an item in an array that passes a filter function.
Array.findLast
works just like Array.find
, except it begins at the end of the array and traverses it in reverse order. Array.findLastIndex
simply returns the item's index, rather than the item itself.
These methods both take a higher-order function as an argument and call this function on each element in the list, returning from the method call when the function returns true
.
The 10.1 release of the V8 engine also includes minor performance improvements, which you can read about in the release notes on the V8 project blog.
Node.js 18 Has A Built-in Test Runner
Node.js has included the assert
library since very early days, making it fairly easy to write simple tests without a third-party library. But good test suites grow quickly, and most projects today depend on a test runner library like Jest, Mocha, Jasmine, or Ava.
These libraries make it easier to organize tests into logical groups, visualize test outcomes, collect coverage data, and simplify setup and teardown.
The node:test
module included in Node.js version 18 adds a lot of functionality for which you would previously need one of these modules.
The built-in test runner supports subtests, test skipping, limited test runs, and callback tests. Here are a few examples of these features in action:
import assert from "assert";
import test from "node:test";
Notice the "node:" prefix, distinguishing this module from any user-created packages named "test".
test("Concatenate user's full name", (t) => {
const user = new User("John", "Doe");
assert.strictEqual(user.fullName(), "John Doe");
});
The test
import is a function used to define tests. The first argument is a string describing the test, and the second is the function containing the test logic. This API is roughly the same as Ava's test
, and takes the place of both it
and describe
from Jest.
It covers the same ground as Jest's describe
because it can also define test groups, with individual tests (or even test subgroups) defined by calling t.test
within the callback function.
For example, we can create a test group for serialization of a class and individual tests for each type of supported serialization.
test("User class serialization", (t) => {
const user = new User("John", "Doe");
await t.test("Serialize to string", (t) => {
const str = user.toString();
assert.ok(str.includes("John"));
assert.ok(str.includes("Doe"));
...
});
await t.test("Serialize to JSON", (t) => {
const userJson = user.toJSON();
assert.doesNotThrow(() => JSON.parse(userJson));
...
});
});
Now, the string passed to test
describes a test group, and each call to t.test
defines an individual test. Note that t.test
returns a promise, hence the await
s in the example.
While it's exciting to see these testing features embraced by the Node.js standard library, I think it's reasonable to expect most projects to use a third-party testing module still.
Many Node devs already have a favorite test library, and it's likely to be a while before the node:test
core module supports all the features that third-party libraries offer.
New Browser-compatible APIs in Node.js 18
Commonly supported browser APIs like fetch and Streams are among the most significant additions to Node.js 18.
In the browser, fetch
replaced the cumbersome XMLHttpRequest
with a flexible, terse HTTP client. The Streams API allows us to perform incremental processing of data that's received from or sent to a network.
It's important to note that these APIs are still considered experimental in Node.js 18. It may be wise to avoid their use in mission-critical applications for the moment, as breaking API updates are not out of the question.
Node.js Now Supports Fetch API
Adding support for fetch
to Node.js allows us to make HTTP requests concisely without:
- Third-party libraries like
axios
ornode-fetch
(which exposes an API roughly equivalent to the new built-in global) - Relying on the more complicated
http
andhttps
packages to make requests from Node projects.
The existing http
and https
packages are very flexible and support advanced functionality. However, the fetch
global function is much more concise; writing Node.js code will feel more natural to developers used to writing JavaScript for the browser. Additionally, because Node.js and modern browsers now share this API, it is easier to write code to run in both environments.
Here's how we would write an HTTPS request in Node.js using
only first-party modules before the addition of the fetch
API:
import https from "https";
const data = {
nameFirst: "John",
nameLast: "Doe",
};
let responseData = "";
const req = https.request(
"https://example.org/user",
{ method: "POST" },
(res) => {
res.on("data", (data) => {
responseData += data;
});
res.on("error", (error) => console.error(error));
}
);
req.end(JSON.stringify(data), "utf-8", () => {
console.log(`Response data: ${responseData}`);
});
Notice the heavy reliance on callbacks and the fact that the https
module does not support promises. We have to register handlers for response data and errors, and be careful not to use the response data variable before the request completes.
While this is a somewhat awkward example, it highlights the
difficulty of using the https
and http
modules in Node, and explains why most developers choose to use a third-party module like Axios, request, or node-fetch.
If we were to make the same request in Node 18 using the fetch
API, it would look something like this:
const data = {
nameFirst: "John",
nameLast: "Doe",
};
try {
const response = await fetch("https://example.org/user", {
method: "POST",
body: JSON.stringify(data),
});
const responseJson = await response.json();
console.log(`Response data: ${responseJson}`);
} catch (error) {
console.error(error);
}
This version is decidedly more terse and familiar to JavaScript devs used to modern language features. I/O operations return promises, errors are thrown and caught, and the code reads synchronously from top to bottom.
While there will certainly still be use cases for the http
and https
packages and their lower-level capabilities, I think most devs will prefer the new fetch
global function for daily use.
Support for Streams API
The Streams API broadly describes a set of interfaces such as ReadableStream
and WritableStream
, which allow us to process data incrementally before a whole object loads in memory. The Streams API is actually a prerequisite for fetch
compatibility because the body
property of a fetch
response is a ReadableStream
.
A common use case for streams is that we want to use a large amount of data, but don't need the entirety of the data to operate on: think multimedia applications or other real-time apps.
While Node.js has always supported these use cases, the availability of the same high-level APIs in both the client and server environments may make developing these applications easier.
Wrap Up: Ecosystem Implications and Node.js 18 Release Notes
One of the selling points of Node.js, and more broadly, JavaScript-based backends, is that full-stack engineers can work at all levels of a web application's stack without switching languages.
It's clear that the Node team is keen on playing to this advantage through Node.js 18's new features, continuing to narrow the gap between JavaScript in the browser and JavaScript on the server. When we consider the increased prevalence of code that needs to run in both environments, such as server-side rendered websites, it's encouraging to see that gap continue to close.
There have been several smaller changes in Node.js 18, such as changes to internationalization APIs, formalization of the Blob and BroadcastChannel APIs, and more.
If you want to dig into the full set of changes in this version, you should take a look at the official Node.js 18 release notes.
I hope you enjoy trying out the new features in this exciting new LTS release of Node.js. Happy coding!
P.S. If you liked this post, subscribe to our JavaScript Sorcery list for a monthly deep dive into more magical JavaScript tips and tricks.
P.P.S. If you need an APM for your Node.js app, go and check out the AppSignal APM for Node.js.
Top comments (0)