Node.js v14 Has Arrived With Some New API Features

Version 14 of Node.js (the server-side Javascript platform) was released on April 14, and it brings several new features, some experimental, that can be a benefit to API providers and consumers. Node.js relies on an internal JavaScript engine called V8 that's built by Google. V8 recently released a new version, 8.1, that also includes new features. This newest version is part of Node.js 14, and as such, those new features are available in Node.js as well. Let's explore some of these new features.

Optional Chaining and Nullish Coalescing

V8 version 8.1, and thus Node.js 14, includes a new JavaScript language feature called Optional Chaining. Although seemingly trivial, this feature is going to prove a huge benefit to API consumers. To understand what it is, let's first look at a situation where you would need it. When you call a web API, you typically use an SDK built for the API (usually offered by the API provider), or you'll use an HTTP library such as request.js, found at https://github.com/request/request. Some APIs provide a flexible response structure, meaning objects may or may not have the members you need.

For example, you might call an API that returns customer information like this:

{ 
    first: 'John', 
    last: 'Smith', 
    phone: '919-555-1212', 
    office: {
        primary: {
            address: '123 Main St',
            city: 'New York',
            state: 'NY',
            zip: '10010'
        },
        secondary: {
            address: '111 First Ave',
            city: 'Chicago',
            state: 'NY',
            zip: '60007'
        }
    }
}

This object contains an office object, that contains a primary member and a secondary member. If you need to access the zip of the secondary member, you can use code such as this:

console.log(obj.office.secondary.zip);

This only works, however, if all the members you're accessing actually exist. Suppose the API only returns a secondary object when the customer has a secondary office. If the entire secondary object is gone, trying to access its zip member will result in an exception, because the secondary object isn't even present:

TypeError: Cannot read property 'zip' of undefined

If you don't have an exception handler, your entire Node.js program will stop. Previously, the way around this is to pack your code with if statements. There are different approaches, but this is a simple one:

if (obj && obj.office && obj.office.secondary && obj.office.secondary.zip) {
    console.log(obj.office.secondary.zip);
}
else {
    // Handle the situation that the secondary office isn't present
}

With the new version, however, the V8 engine allows you to use optional chaining. Here's how you use it:

console.log(obj.office.secondary?.zip);

No "if" statement is needed. Notice the question mark after the word secondary. This tells node that you're not sure if the office object will contain an object called secondary, and if not, to just halt the processing of the expression and return undefined. So if the secondary object is present, the console.log will print out the zip like so:

60007

And if the object isn't present, it will print out undefined:

undefined

You'll likely still need an "if" statement, but your code will be much simpler. For example, you might need to put the question mark in multiple places in the expression. Then you can try to store the zip in a variable, and then test if the variable contains undefined or not:

var zip = obj?.office?.secondary?.zip;
if (zip) {
    console.log(`The secondary office zip is ${zip}.`);
}
else {
    console.log('No secondary office zip code found.');
}

The advantage to putting the question mark after the initial obj is you don't need to first test if the request returned an object at all. Of course, you'll still need to handle errors appropriately; if the object comes back null because the customer doesn't exist, you'll want a separate error message from simply stating that no secondary office zip code was found. But in any case, your code will be simpler with fewer lines. And fewer lines of code means less chances for bugs.

Another new feature in V8 fits closely with optional chaining is called the nullish coalescing operator. Prior to the latest V8, if you wanted to provide a default value in an expression to use if a member doesn't exist, you could use the logical "OR" operator, like so:

console.log(obj.value || "empty");

This would print out obj.value, unless obj doesn't have a value member, in which case it would print out the word "empty". However, there's a flaw here. If obj.value is 0 or an empty string, "", this line of code will still print out the word "empty". That's because in logical OR expressions, the number 0, the empty string, the value null, and the value undefined all equate to a false value.

The newest V8 provides a new operator that you can use instead. It's called a nullish coalescing operator and it consists of two question marks. It behaves almost exactly like the above OR operator, except only the null and undefined values get passed through to the default value. So you can use this instead:

console.log(obj.value ?? "empty");

Now if the value is 0 or an empty string, you'll get back the respective value, 0 or empty string. Only if the value is null or undefined will you get the default value, in this case the word "empty".

Now go back to the optional chaining feature. The nullish coalescing operator can help you provide a default value, like so:

var custzip = obj.office.secondary?.zip ?? "No secondary zip code";
console.log(custzip);

Here, if the secondary object is present and it has a zip member, the custzip variable will receive the zip code. But if either the secondary object or zip member is missing, the expression to the left of ?? will give back undefined, and the ?? will transform that undefined into the string "No secondary zip code."

Async Local Storage

This next new feature is still in the experimental stage, which means you'll want to try it and test it out, but not use it in production. The feature is called async local storage, and can be a benefit especially to API providers.

The idea behind async local storage is to tie storage to a series of code sections that run asynchronously. If you've used HTTP frameworks for Node such as Express.js, you've likely used a sort of asynchronous local storage, because Express provides its own implementation that's not native to Node.

The idea is that you might have three functions that get called in sequence in response to a single web request in an API provider system. The first one might look up data based on the user making the HTTP request, and save it into a storage area. The second might do another database lookup, and the third might send the combined data back to the user. The tricky part is that another user on the web (or possibly thousands of users) might also be calling your web server at that same time. And each of the three functions needs its own separate storage areas for each request.

We're not talking about session storage here. We're talking about storage per HTTP request. But we still want the storage we're using to be unique among requests, but shared across all three functions.

There are already lots of examples on the web that claim to demonstrate how to use the local storage, but most of them are rather contrived and use setTimeout. Perhaps a better, more useful example would be one that uses the async.js library (found at https://caolan.github.io/async/v3/), and the MongoDB native driver (found at https://github.com/mongodb/node-mongodb-native) available through these npm calls:

npm install --save async
npm install --save mongodb

The following code is a modified version of the async local storage example found in the official docs at https://nodejs.org/api/async_hooks.html#async_hooks_class_asynclocalstorage. But instead of using a setImmediate, it does two actual database lookups. The code uses a common pattern where the database lookups take place inside async waterfall functions. But instead of storing the results of the database in a local variable, it stores the results in a store.

Here's the code:

// Load required modules
const http = require('http');
const { AsyncLocalStorage } = require('async_hooks');
const async = require('async');
const MongoClient = require('mongodb').MongoClient;
const asyncLocalStorage = new AsyncLocalStorage();

function getdata(cb) {
    // Connect to the mongo database server
    MongoClient.connect('mongodb://localhost:27017', function(err, client) {
        // Load the database
        const db = client.db('testdb1');
        // Start the sequence of functions, i.e. "waterfall"
        async.waterfall([
            function (wcb) {
                // Open the customers collection and get one customer
                const coll = db.collection('customers');
                coll.findOne({token:'1111'}, function(err,cust){
                    asyncLocalStorage.getStore().customer = cust;
                    wcb();
                });
            },
            function (wcb) {
                // Open the orders collection and get all
                // orders for this one customer. Convert it
                // to an array as well.
                const coll = db.collection('orders');
                coll.find({customer:
                  asyncLocalStorage.getStore().customer._id})
                  .toArray(function(err,orders){
                    asyncLocalStorage.getStore().orders = orders;
                    wcb();
                });
            }
        ], function() {
            // Final step, close the database and call
            // the callback function
            client.close();
            cb();
        });
    });    
}

// Keep an individual count of each time the
// store is created. Store it in the idSeq variable.
let idSeq = 0;
// Create the web server
http.createServer((req, res) => {
    // Create the store for this HTTP request
    var store = { id: idSeq++ };
    // We wrap the store in a call to "run".
    asyncLocalStorage.run(store, () => {
        // Call our getdata function
        getdata(function() {
            // Send the data back to the consumer
            res.end(JSON.stringify(asyncLocalStorage.getStore()));
        }, 1000);
    });
}).listen(3000);

This code creates a store with each incoming HTTP request by calling asyncLocalStorage.run. Remember, we're still not talking about sessions; the HTTP server is still sessionless. But the store works like a global variable accessible from any of the callbacks that are triggered from within the run call. And you could certainly do as we've done in the past, and simply pass an object around to all the calls, and fill its members accordingly, and send it out with a callback. But this local storage approach takes away the complexity of passing data around, and ensures that the store is always there whenever you need it. To grab it, you just call asyncLocalStorage.getStore().

You can see then how the waterfall functions grab the store anytime they need it. They do a database query and store the results in the store object. Then at the end of the request, the code calls res.end and sends the stringified JSON data back to the caller. Note, however, that I've hardcoded the customer token lookup to 1111. In a "real" application you would get that from a cookie that was likely created with a login, or if it's a REST call, from the URL making the call.

If you want to try this out yourself, here's a script to paste into MongoDB to initialize the data:

use testdb1;
db.customers.insert({ "_id" : ObjectId("5eab167ba24567257179fcbf"), "name" : "John Smith", "phone" : "9195551212", "token" : "1111" });
db.customers.insert({ "_id" : ObjectId("5eab1682a24567257179fcc0"), "name" : "Sally Jones", "phone" : "6165551212", "token" : "1112" });
db.orders.insert({ "_id" : ObjectId("5eab1b156d35168ded88f846"), "customer" : ObjectId("5eab167ba24567257179fcbf"), "sku" : "SKU123", "total" : 12.35 });
db.orders.insert({ "_id" : ObjectId("5eab1b1f6d35168ded88f847"), "customer" : ObjectId("5eab167ba24567257179fcbf"), "sku" : "SKU555", "total" : 25.5 });

Then when you run the server code, in another command prompt you can make multiple calls to the code with this:

curl localhost:3000/
curl localhost:3000/
curl localhost:3000/

With each run to curl, you will get back another object. Look at the id of the object and you'll see it's different each time.

Diagnostic Reports

Version 12 of Node had an experimental feature called Diagnostic Reports. With this new version 14, that feature is now stable, and no longer experimental. That means you're free to start using it in your production code.

Obtaining a diagnostic report is a simple matter of one function call. Save the following code to a file and run the file under node:

function test() {
    process.report.writeReport();
}
test();
console.log('Ready');

The line inside the test function is the single call, writeReport, which saves the diagnostic report to a file. The program continues to run, as you'll see by the line Ready being written to the console.

After you run this, you'll see a new file containing JSON. When you open it you can see a great deal of information, such as the version of Node that's running; information about the computer itself; a full stack trace for where the call to writeReport occurred in the code; full information on the JavaScript memory heap (much like what you can see in Chrome's debug tools); environment variables; and more.

This is only the surface of what's available for diagnostic reports. There are two excellent resources for learning about them; first is a Medium article and second is the official documentation.

Web Assembly

Another interesting feature in Node 14 is an implementation of WebAssembly System Interface (WASI). Like the async local storage, this is also an experimental feature. But if you're building high-performance apps, whether API providers or consumers, you might want to try this out. Node already has a reputation for being blindingly fast, but there's always room for improvement.

We don't have space here to teach you how to use WebAssembly, as it's an entire programming language that runs inside the V8 engine that's been likened to the idea of compiled Javascript. However, what follows is a very quick demonstration of it in action. Be sure to look at the official documentation at https://nodejs.org/api/wasi.html, and especially note the reminder at the bottom of the example that you have to provide some runtime arguments to Node to activate this feature.

First, here's a web assembly program, which I borrowed from this example. https://webassembly.github.io/wabt/demo/wat2wasm/.

(module
  (func (export "addTwo") (param i32 i32) (result i32)
    local.get 0
    local.get 1
    i32.add))

Save this in a file called simple.wat. This code module exports a function called addTwo, which takes two integers and returns the sum.

To compile this code, you'll need some tools that you can find at https://github.com/webassembly/wabt. Follow the steps for installing the tools and adding them to your path. Then you can compile the code like so:

wat2wasm simple.wat -o simple.wasm

This will create a binary Web Assembly file called simple.wasm. Finally, create a file called app.js with the following:

const fs = require('fs');
const { WASI } = require('wasi');
const wasi = new WASI({});
const importObject = { wasi_snapshot_preview1: wasi.wasiImport };

WebAssembly.compile(fs.readFileSync('./simple.wasm'))
.then(function(wasm) {
    WebAssembly.instantiate(wasm, importObject).then(function(instance)
    {
        var addTwo = instance.exports.addTwo;
        var test = addTwo(5,6);
        console.log(test);
    });
});

Run the code with these required command-line parameters:

node --experimental-wasi-unstable-preview1 --experimental-wasm-bigint app.js

This code will load the simple.wasm you built from the wat2wasm tool; compile the code; instantiate a WebAssembly instance with that code, and then save the compiled code into an object called instance. You can access the compiled code through the instance.exports object; in this case, the object has a member called addTwo, the same as the function name in the original WebAssembly code. This JavaScript code grabs that function and saves it into a local variable simply called addTwo. Then it calls the function just like it would any other JavaScript function, except this is compiled WebAssembly code. In this case, it passes 5 and 6, saves the result (11) in a variable called test, and prints out the variable.

Conclusion

Be sure to check out the official announcement from the Node JS team, here. Also check out the official documentation at https://nodejs.org/api/. Although some of these new features might seem trivial, such as optional chaining, they can be pretty useful in APIs, both for providers and consumers. Make sure you pay close attention to what's still considered "experimental" and don't use those features in production code. But practice with them, because they will likely become stable features in a future release of Node, hopefully sooner than later.

Be sure to read the next Developers article: Google Ending Support for JSON-RPC and Global HTTP Batch

 

Comments (0)