Node.js Web Development - Fifth Edition

5 (2 reviews total)
By David Herron
    Advance your knowledge in tech with a Packt subscription

  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. About Node.js

About this book

Node.js is the leading choice of server-side web development platform, enabling developers to use the same tools and paradigms for both server-side and client-side software. This updated fifth edition of Node.js Web Development focuses on the new features of Node.js 14, Express 4.x, and ECMAScript, taking you through modern concepts, techniques, and best practices for using Node.js.

The book starts by helping you get to grips with the concepts of building server-side web apps with Node.js. You’ll learn how to develop a complete Node.js web app, with a backend database tier to help you explore several databases. You'll deploy the app to real web servers, including a cloud hosting platform built on AWS EC2 using Terraform and Docker Swarm, while integrating other tools such as Redis and NGINX. As you advance, you'll learn about unit and functional testing, along with deploying test infrastructure using Docker. Finally, you'll discover how to harden Node.js app security, use Let's Encrypt to provision the HTTPS service, and implement several forms of app security with the help of expert practices. With each chapter, the book will help you put your knowledge into practice throughout the entire life cycle of developing a web app.

By the end of this Node.js book, you’ll have gained practical Node.js web development knowledge and be able to build and deploy your own apps on a public web hosting solution.

Publication date:
July 2020
Publisher
Packt
Pages
760
ISBN
9781838987572

 
About Node.js

JavaScript is at the fingertips of every frontend web developer, making it a very popular programming language, so much so that it is stereotyped as being for client-side code in web pages. The chances are that, having picked up this book, you've heard of Node.js, a programming platform for coding in JavaScript outside web browsers. Now about ten years old, Node.js is becoming a mature programming platform that's widely used in projects both big and small.

This book will give you an introduction to Node.js. By the end of this book, you will have learned about the complete lifecycle of developing server-side web applications using Node.js, from concept to deployment and security. In writing this book, we have presumed the following:

  • You already know how to write software.
  • You are familiar with JavaScript.
  • You know something about developing web applications in other languages.

When we evaluate a new programming tool, do we latch on because it's the popular new tool? Maybe some of us do that, but the mature approach is to weigh one tool against another. That's what this chapter is about, presenting the technical rationale for using Node.js. Before getting to the code, we must consider what Node.js is and how it fits in the overall marketplace of software development tools. Then we will dive right into developing working applications and recognize that often the best way to learn is by rummaging around in working code.

We will cover the following topics in this chapter:

  • An introduction to Node.js
  • What you can do with Node.js
  • Why you should use Node.js
  • The architecture of Node.js
  • Performance, utilization, and scalability with Node.js
  • Node.js, microservice architecture, and testing
  • Implementing the twelve-factor app model with Node.js
 

Overview of Node.js

Node.js is an exciting new platform for developing web applications, application servers, any sort of network server or client, and general-purpose programming. It is designed for extreme scalability in networked applications through an ingenious combination of server-side JavaScript, asynchronous I/O, and asynchronous programming.

While only ten years old, Node.js has quickly grown in prominence and is now playing a significant role. Companies, both large and small, are using it for large-scale and small-scale projects. PayPal, for example, has converted many services from Java to Node.js.

The Node.js architecture departs from a typical choice made by other application platforms. Where threads are widely used to scale an application to fill the CPU, Node.js eschews threads because of their inherent complexity. It's claimed that with single-thread event-driven architectures, the memory footprint is low, throughput is high, the latency profile under load is better, and the programming model is simpler. The Node.js platform is in a phase of rapid growth, and many see it as a compelling alternative to the traditional web application architectures using Java, PHP, Python, or Ruby on Rails.

At its heart, it is a standalone JavaScript engine with extensions that is suitable for general-purpose programming and that has a clear focus on application server development. Even though we're comparing Node.js to application-server platforms, it is not an application server. Instead, Node.js is a programming runtime akin to Python, Go, or Java SE. While there are web application frameworks and application servers written in Node.js, it is simply a system to execute JavaScript programs.

The key architectural choice is that Node.js is event-driven, rather than multithreaded. The Node.js architecture rests on dispatching blocking operations to a single-threaded event loop, with results arriving back to the caller as an event that invokes an event handler function. In most cases, the event is converted into a promise that is handled by an async function. Because Node.js is based on Chrome's V8 JavaScript engine, the performance and feature improvements implemented in Chrome quickly flow through to the Node.js platform.

The Node.js core modules are general enough to implement any sort of server that is executing any TCP or UDP protocol, whether it's a Domain Name System (DNS), HTTP, internet relay chat (IRC), or FTP. While it supports the development of internet servers or clients, its biggest use case is regular website development, in place of technology such as an Apache/PHP or Rails stack, or to complement existing websites—for example, adding real-time chat or monitoring existing websites can easily be done with the Socket.IO library for Node.js. Its lightweight, high-performance nature often sees Node.js used as a glue service.

A particularly intriguing combination is the deployment of small services on modern cloud infrastructure using tools such as Docker and Kubernetes, or function-as-a-service platforms, such as AWS Lambda. Node.js works well when dividing a large application into easily deployable microservices at scale.

With a high-level understanding of Node.js under our belt, let's dig a little deeper.

 

The capabilities of Node.js

Node.js is a platform for writing JavaScript applications outside web browsers. This is not the JavaScript environment we are familiar with in web browsers! While Node.js executes the same JavaScript language that we use in browsers, it doesn't have some of the features associated with the browser. For example, there is no HTML DOM built into Node.js.

Beyond its native ability to execute JavaScript, the built-in modules provide capabilities of the following sort:

  • Command-line tools (in shell script style)
  • An interactive-terminal style of program—that is, a read-eval-print loop (REPL)
  • Excellent process control functions to oversee child processes
  • A buffer object to deal with binary data
  • TCP or UDP sockets with comprehensive, event-driven callbacks
  • DNS lookup
  • An HTTP, HTTPS, and HTTP/2-client server layered on top of the TCP library filesystem access
  • Built-in rudimentary unit testing support through assertions

The network layer of Node.js is low level while being simple to use—for example, the HTTP modules allow you to write an HTTP server (or client) using a few lines of code. This is powerful, but it puts you, the programmer, very close to the protocol requests and makes you implement precisely those HTTP headers that you should return in request responses.

Typical web-application developers don't need to work at a low level of the HTTP or other protocols; instead, we tend to be more productive working with higher-level interfaces—for example, PHP coders assume that Apache/Nginx/and so on are already there providing the HTTP, and that they don't have to implement the HTTP server portion of the stack. By contrast, a Node.js programmer does implement an HTTP server, to which their application code is attached.

To simplify the situation, the Node.js community has several web application frameworks, such as Express, providing the higher-level interfaces required by typical programmers. You can quickly configure an HTTP server with baked-in capabilities, such as sessions, cookies, serving static files, and logging, letting developers focus on their business logic. Other frameworks provide OAuth 2 support or focus on REST APIs, and so on.

The community of folks using Node.js has built an amazing variety of things on this foundation.

What are folks doing with Node.js?

Node.js is not limited to web service application development; the community around Node.js has taken it in many other directions:

  • Build tools: Node.js has become a popular choice for developing command-line tools that are used in software development or communicating with service infrastructure. Grunt, Gulp, and Webpack are widely used by frontend developers to build assets for websites. Babel is widely used for transpiling modern ES-2016 code to run on older browsers. Popular CSS optimizers and processors, such as PostCSS, are written in Node.js. static website generation systems, such as Metalsmith, Punch, and AkashaCMS, run at the command line, and generate website content that you upload to a web server.
  • Web UI testing: Puppeteer gives you control over a headless Chrome web-browser instance. With it, you can develop Node.js scripts by controlling a modern, full-featured web browser. Some typical use cases are web scraping and web application testing.
  • Desktop applications: Both Electron and node-webkit (NW.js) are frameworks for developing desktop applications for Windows, macOS, and Linux. These frameworks utilize a large chunk of Chrome, wrapped by Node.js libraries, to develop desktop applications using web UI technologies. Applications are written with modern HTML5, CSS3, and JavaScript, and can utilize leading-edge web frameworks, such as Bootstrap, React, VueJS, and AngularJS. Many popular applications have been built using Electron, including the Slack desktop client application, the Atom, Microsoft Visual Code programming editors, the Postman REST client, the GitKraken GIT client, and Etcher, which makes it incredibly easy to burn OS images to flash drives to run on single-board computers.
  • Mobile applications: The Node.js for Mobile Systems project lets you develop smartphone or tablet computer applications using Node.js for both iOS and Android. Apple's App Store rules preclude incorporating a JavaScript engine with JIT capabilities, meaning that normal Node.js cannot be used in an iOS application. For iOS application development, the project uses Node.js-on-ChakraCore to skirt around the App Store rules. For Android application development, the project uses regular Node.js on Android. At the time of writing, the project is in an early stage of development, but it looks promising.
  • Internet of things (IoT): Node.js is a very popular language for Internet-of-Things projects, and Node.js runs on most ARM-based, single-board computers. The clearest example is the NodeRED project. It offers a graphical programming environment, letting you draw programs by connecting blocks together. It features hardware-oriented input and output mechanisms—for example, to interact with General Purpose I/O (GPIO) pins on Raspberry Pi or Beaglebone single-board computers.

You may already be using Node.js applications without realizing it! JavaScript has a place outside the web browser, and it's not just thanks to Node.js.

Server-side JavaScript

Quit scratching your head, already! Of course, you're doing it, scratching your head and mumbling to yourself, "What's a browser language doing on the server?" In truth, JavaScript has a long and largely unknown history outside the browser. JavaScript is a programming language, just like any other language, and the better question to ask is "Why should JavaScript remain trapped inside web browsers?"

Back in the dawn of the web age, the tools for writing web applications were at a fledgling stage. Some developers were experimenting with Perl or TCL to write CGI scripts, and the PHP and Java languages had just been developed. Even then, JavaScript saw use on the server side. One early web application server was Netscape's LiveWire server, which used JavaScript. Some versions of Microsoft's ASP used JScript, their version of JavaScript. A more recent server-side JavaScript project is the RingoJS application framework in the Java universe. Java 6 and Java 7 were both shipped with the Rhino JavaScript engine. In Java 8, Rhino was dropped in favor of the newer Nashorn JavaScript engine.

In other words, JavaScript outside the browser is not a new thing, even if it is uncommon.

You have learned that Node.js is a platform for writing JavaScript applications outside of web browsers. The Node.js community uses this platform for a huge array of application types, far more than was originally conceived for the platform. This proves that Node.js is popular, but we must still consider the technical rationale for using it.

 

Why should you use Node.js?

Of the many available web-application development platforms, why should you choose Node.js? There are many stacks to choose from; what is it about Node.js that makes it rise above the others? We will learn the answer to this in the following sections.

Popularity

Node.js is quickly becoming a popular development platform, and is being adopted by plenty of big and small players. One of these players is PayPal, who are replacing their incumbent Java-based system with one written in Node.js. Other large Node.js adopters include Walmart's online e-commerce platform, LinkedIn, and eBay.

For PayPal's blog post about this, visit https://www.paypal-engineering.com/2013/11/22/node-js-at-paypal/.

According to NodeSource, Node.js usage is growing rapidly (for more information, visit https://nodesource.com/node-by-numbers). The evidence for this growth includes increasing bandwidth for downloading Node.js releases, increasing activity in Node.js-related GitHub projects, and more.

Interest in JavaScript itself remains very strong but has been at a plateau for years, measured in search volume (Google Insights) and its use as a programming skill (Dice Skills Center). Node.js interest has been growing rapidly, but is showing signs of plateauing.

It's best to not just follow the crowd because there are different crowds, and each one claims that their software platform does cool things. Node.js does some cool things, but what is more important is its technical merit.

JavaScript everywhere

Having the same programming language on the server and client has been a long-time dream on the web. This dream dates back to the early days of Java, where Java applets in the browser were to be the frontend to server applications written in Java, and JavaScript was originally envisioned as a lightweight scripting language for those applets. Java never fulfilled its hype as a client-side programming language, and even the phrase "Java Applets" is fading into a dim memory of the abandoned client-side application model. We ended up with JavaScript as the principle in-browser, client-side language, rather than Java. Typically, the frontend JavaScript developers were in a different language universe than the server-side team, which was likely to be coding in PHP, Java, Ruby, or Python.

Over time, in-browser JavaScript engines became incredibly powerful, letting us write ever-more-complex browser-side applications. With Node.js, we are finally able to implement applications with the same programming language on the client and server by having JavaScript at both ends of the web, in the browser and server.

A common language for frontend and backend offers several potential benefits:

  • The same programming staff can work on both ends of the wire.
  • Code can be migrated between the server and client more easily.
  • Common data formats (JSON) between the server and client.
  • Common software tools exist for the server and client.
  • Common testing or quality-reporting tools for the server and client.
  • When writing web applications, view templates can be used on both sides.

The JavaScript language is very popular because of its ubiquity in web browsers. It compares favorably with other languages while having many modern, advanced language concepts. Thanks to its popularity, there is a deep talent pool of experienced JavaScript programmers out there.

Leveraging Google's investment in V8

To make Chrome a popular and excellent web browser, Google invested in making V8 a super-fast JavaScript engine. Google, therefore, has a huge motivation to keep on improving V8. V8 is the JavaScript engine for Chrome, and it can also be executed in a standalone manner.

Node.js is built on top of the V8 JavaScript engine, letting it take advantage of all that work on V8. As a result, Node.js was able to quickly adopt new JavaScript language features as they were implemented by V8 and reap performance wins for the same reason.

Leaner, asynchronous, event-driven model

The Node.js architecture, built on a single execution thread, with an ingenious event-oriented, asynchronous-programming model, and a fast JavaScript engine, is claimed to have less overhead than thread-based architectures. Other systems using threads for concurrency tend to have memory overhead and complexity, which Node.js does not have. We'll get into this more later in the chapter.

Microservice architecture

A new sensation in software development is the idea of the microservice. Microservices are focused on splitting a large web application into small, tightly-focused services that can be easily developed by small teams. While they aren't exactly a new idea—they're more of a reframing of old client–server computing models—the microservice pattern fits well with agile project-management techniques, and gives us a more granular application deployment.

Node.js is an excellent platform for implementing microservices. We'll get into this later.

Node.js is stronger after a major schism and hostile fork

During 2014 and 2015, the Node.js community faced a major split over policy, direction, and control. The io.js project was a hostile fork driven by a group that wanted to incorporate several features and change who was in the decision-making process. The end result was a merge of the Node.js and io.js repositories, an independent Node.js foundation to run the show, and the community working together to move forward in a common direction.

A concrete result of healing that rift is the rapid adoption of new ECMAScript language features. The V8 engine is adopting these new features quickly to advance the state of web development. The Node.js team, in turn, is adopting these features as quickly as they show up in V8, meaning that promises and async functions are quickly becoming a reality for Node.js programmers.

The bottom line is that the Node.js community not only survived the io.js fork and the later ayo.js fork, but the community and the platform it nurtured grew stronger as a result.

In this section, you have learned several reasons to use Node.js. Not only is it a popular platform, with a strong community behind it, but there are also serious technical reasons to use it. Its architecture has some key technical benefits, so let's take a deeper look at these.

 

The Node.js event-driven architecture

Node.js's blistering performance is said to be because of its asynchronous event-driven architecture and its use of the V8 JavaScript engine. This enables it to handle multiple tasks concurrently, such as juggling between requests from multiple web browsers. The original creator of Node.js, Ryan Dahl, followed these key points:

  • A single-thread, event-driven programming model is simpler to code and has less complexity and less overhead than application servers that rely on threads to handle multiple concurrent tasks.
  • By converting blocking function calls into asynchronous code execution, you can configure the systems so that it issues an event when the blocking request is satisfied.
  • You can leverage the V8 JavaScript engine from the Chrome browser, and all the work goes into improving V8; all the performance enhancements going into V8, therefore, benefits Node.js.

In most application servers, concurrency, or the ability to handle multiple concurrent requests, is implemented with a multithreaded architecture. In such a system, any request for data, or any other blocking function call, causes the current execution thread to suspend and wait for the result. Handling concurrent requests requires there to be multiple execution threads. When one thread is suspended, another thread can execute. This causes churn as the application server starts and stops the threads to handle requests. Each suspended thread (typically waiting on an input/output operation to finish) consumes a full call stack of memory, adding to overhead. Threads add complexity to the application server as well as server overhead.

To help us wrap our heads around why this would be, Ryan Dahl, the creator of Node.js, offered the following example. In his Cinco de NodeJS presentation in May 2010 (https://www.youtube.com/watch?v=M-sc73Y-zQA) Dahl asked us what happens when we execute a line of code such as this:

result = query('SELECT * from db.table'); 
// operate on the result 

Of course, the program pauses at this point while the database layer sends the query to the database and waits for the result or the error. This is an example of a blocking function call. Depending on the query, this pause can be quite long (well, a few milliseconds, which is ages in computer time). This pause is bad because the execution thread can do nothing while it waits for the result to arrive. If your software is running on a single-threaded platform, the entire server would be blocked and unresponsive. If instead your application is running on a thread-based server platform, a thread-context switch is required to satisfy any other requests that arrive. The greater the number of outstanding connections to the server, the greater the number of thread-context switches. Context switching is not free because more threads require more memory per thread state and more time for the CPU to spend on thread management overheads.

The key inspiration guiding the original development of Node.js was the simplicity of a single-threaded system. A single execution thread means that the server doesn't have the complexity of multithreaded systems. This choice meant that Node.js required an event-driven model for handling concurrent tasks. Instead of the code waiting for results from a blocking request, such as retrieving data from a database, an event is instead dispatched to an event handler.

Using threads to implement concurrency often comes with admonitions, such as expensive and error-prone, the error-prone synchronization primitives of Java, or designing concurrent software can be complex and error-prone. The complexity comes from access to shared variables and various strategies to avoid deadlock and competition between threads. The synchronization primitives of Java are an example of such a strategy, and obviously many programmers find them difficult to use. There's a tendency to create frameworks such as java.util.concurrent to tame the complexity of threaded concurrency, but some argue that papering over complexity only makes things more complex.

A typical Java programmer might object at this point. Perhaps their application code is written against a framework such as Spring, or maybe they're directly using Java EE. In either case, their application code does not use concurrency features or deal with threads, and therefore where is the complexity that we just described? Just because that complexity is hidden within Spring and Java EE does not mean that there is no complexity and overhead.

Okay, we get it: while multithreaded systems can do amazing things, there is inherent complexity. What does Node.js offer?

The Node.js answer to complexity

Node.js asks us to think differently about concurrency. Callbacks fired asynchronously from an event loop are a much simpler concurrency model—simpler to understand, simpler to implement, simpler to reason about, and simpler to debug and maintain.

Node.js has a single execution thread with no waiting on I/O or context switching. Instead, there is an event loop that dispatches events to handler functions as things happen. A request that would have blocked the execution thread instead executes asynchronously, with the results or errors triggering an event. Any operation that would block or otherwise take time to complete must use the asynchronous model.

The original Node.js paradigm delivered the dispatched event to an anonymous function. Now that JavaScript has async functions, the Node.js paradigm is shifting to deliver results and errors via a promise that is handled by the await keyword. When an asynchronous function is called, control quickly passes to the event loop rather than causing Node.js to block. The event loop continues handling the variety of events while recording where to send each result or error.

By using an asynchronous event-driven I/O, Node.js removes most of this overhead while introducing very little of its own.

One of the points Ryan Dahl made in the Cinco de Node presentation is a hierarchy of execution time for different requests. Objects in memory are more quickly accessed (in the order of nanoseconds) than objects on disk or objects retrieved over the network (milliseconds or seconds). The longer access time for external objects is measured in zillions of clock cycles, which can be an eternity when your customer is sitting at their web browser ready to move on if it takes longer than two seconds to load the page.

Therefore, concurrent request handling means using a strategy to handle the requests that take longer to satisfy. If the goal is to avoid the complexity of a multithreaded system, then the system must use asynchronous operations as Node.js does.

What do these asynchronous function calls look like?

Asynchronous requests in Node.js

In Node.js, the query that we looked at previously will read as follows:

query('SELECT * from db.table', function (err, result) { 
    if (err) throw err; // handle errors 
    // operate on result 
}); 

The programmer supplies a function that is called (hence the name callback function) when the result (or error) is available. The query function still takes the same amount of time. Instead of blocking the execution thread, it returns to the event loop, which is then free to handle other requests. The Node.js will eventually fire an event that causes this callback function to be called with the result or error indication.

A similar paradigm is used in client-side JavaScript, where we write event handler functions all the time.

Advances in the JavaScript language have given us new options. When used with ES2015 promises, the equivalent code would look like this:

query('SELECT * from db.table') 
.then(result => { 
    // operate on result 
}) 
.catch(err => { 
    // handle errors 
}); 

This is a little better, especially in instances of deeply nested event handling.

The big advance came with the ES-2017 async function:

try {
const result = await query('SELECT * from db.table');
// operate on result
} catch (err) {
// handle errors
}

Other than the async and await keywords, this looks like code we'd write in other languages, and is much easier to read. Because of what await does, it is still asynchronous code execution.

All three of these code snippets perform the same query that we wrote earlier. Instead of query being a blocking function call, it is asynchronous and does not block the execution thread.

With both the callback functions and the promise's asynchronous coding, Node.js had its own complexity issue. Oftentimes, we call one asynchronous function after another. With callback functions, that meant deeply nested callback functions, and with promises, that meant a long chain of .then handler functions. In addition to the complexity of the coding, we have errors and results landing in unnatural places. Instead of landing on the next line of code, the asynchronously executed callback function is invoked. The order of execution is not one line after another, as it is in synchronous programming languages; instead, the order of execution is determined by the order of the callback function execution.

The async function approach solves that coding complexity. The coding style is more natural since the results and errors land in the natural place, at the next line of code. The await keyword integrates asynchronous result handling without blocking the execution thread. A lot is buried under the covers of the async/await feature, and we'll be covering this model extensively throughout this book.

But does the asynchronous architecture of Node.js actually improve performance?

Performance and utilization

Some of the excitement over Node.js is due to its throughput (the requests per second that it can serve). Comparative benchmarks of similar applications—for example, Apache—show that Node.js has tremendous performance gains.

One benchmark going around is the following simple HTTP server (borrowed from https://nodejs.org/en/), which simply returns a Hello World message directly from memory:

var http = require('http'); 
http.createServer(function (req, res) { 
  res.writeHead(200, {'Content-Type': 'text/plain'}); 
  res.end('Hello World\n'); 
}).listen(8124, "127.0.0.1"); 
console.log('Server running at http://127.0.0.1:8124/'); 

This is one of the simpler web servers that you can build with Node.js. The http object encapsulates the HTTP protocol, and its http.createServer method creates a whole web server, listening on the port specified in the listen method. Every request (whether a GET or POST on any URL) on that web server calls the provided function. It is very simple and lightweight. In this case, regardless of the URL, it returns a simple text/plain that is the Hello World response.

Ryan Dahl showed a simple benchmark in a video titled Ryan Dahl: Introduction to Node.js (on the YUI Library channel on YouTube, https://www.youtube.com/watch?v=M-sc73Y-zQA). It used a similar HTTP server to this, but that returned a one-megabyte binary buffer; Node.js gave 822 req/sec, while Nginx gave 708 req/sec, for a 15% improvement over Nginx. He also noted that Nginx peaked at four megabytes of memory, while Node.js peaked at 64 megabytes.

The key observation was that Node.js, running an interpreted, JIT-compiled, high-level language, was about as fast as Nginx, built of highly optimized C code, while running similar tasks. That presentation was in May 2010, and Node.js has improved hugely since then, as shown in Chris Bailey's talk that we referenced earlier.

Yahoo! search engineer Fabian Frank published a performance case study of a real-world search query suggestion widget implemented with Apache/PHP and two variants of Node.js stacks (http://www.slideshare.net/FabianFrankDe/nodejs-performance-case-study). The application is a pop-up panel showing search suggestions as the user types in phrases using a JSON-based HTTP query. The Node.js version could handle eight times the number of requests per second with the same request latency. Fabian Frank said both Node.js stacks scaled linearly until CPU usage hit 100%.

LinkedIn did a massive overhaul of their mobile app using Node.js for the server-side to replace an old Ruby on Rails app. The switch lets them move from 30 servers down to 3, and allowed them to merge the frontend and backend team because everything was written in JavaScript. Before choosing Node.js, they'd evaluated Rails with Event Machine, Python with Twisted, and Node.js, chose Node.js for the reasons that we just discussed. For a look at what LinkedIn did, see http://arstechnica.com/information-technology/2012/10/a-behind-the-scenes-look-at-linkedins-mobile-engineering/.

Most existing Node.js performance tips tend to have been written for older V8 versions that used the CrankShaft optimizer. The V8 team has completely dumped CrankShaft, and it has a new optimizer called TurboFan—for example, under CrankShaft, it was slower to use try/catch, let/const, generator functions, and so on. Therefore, common wisdom said to not use those features, which is depressing because we want to use the new JavaScript features because of how much it has improved the JavaScript language. Peter Marshall, an engineer on the V8 team at Google, gave a talk at Node.js Interactive 2017 claiming that, using TurboFan, you should just write natural JavaScript. With TurboFan, the goal is for across-the-board performance improvements in V8. To view the presentation, see the video titled High Performance JS in V8 at https://www.youtube.com/watch?v=YqOhBezMx1o.

A truism about JavaScript is that it's no good for heavy computation work because of the nature of JavaScript. We'll go over some ideas that are related to this in the next section. A talk by Mikola Lysenko at Node.js Interactive 2016 went over some issues with numerical computing in JavaScript, and some possible solutions. Common numerical computing involves large numerical arrays processed by numerical algorithms that you might have learned in calculus or linear algebra classes. What JavaScript lacks is multidimensional arrays and access to certain CPU instructions. The solution that he presented is a library to implement multidimensional arrays in JavaScript, along with another library full of numerical computing algorithms. To view the presentation, see the video titled Numerical Computing in JavaScript by Mikola Lysenko at https://www.youtube.com/watch?v=1ORaKEzlnys.

At the Node.js Interactive conference in 2017, IBM's Chris Bailey made a case for Node.js being an excellent choice for highly scalable microservices. Key performance characteristics are I/O performance (measured in transactions per second), startup time (because that limits how quickly your service can scale up to meet demand), and memory footprint (because that determines how many application instances can be deployed per server). Node.js excels on all those measures; with every subsequent release, it either improves on each measure or remains fairly steady. Bailey presented figures comparing Node.js to a similar benchmark written in Spring Boot showing Node.js to perform much better. To view his talk, see the video titled Node.js Performance and Highly Scalable Micro-Services - Chris Bailey, IBM at https://www.youtube.com/watch?v=Fbhhc4jtGW4.

The bottom line is that Node.js excels at event-driven I/O throughput. Whether a Node.js program can excel at computational programs depends on your ingenuity in working around some limitations in the JavaScript language.

A big problem with computational programming is that it prevents the event loop from executing. As we will see in the next section, that can make Node.js look like a poor candidate for anything.

Is Node.js a cancerous scalability disaster?

In October 2011, a blog post (since pulled from the blog where it was published) titled Node.js is a cancer called Node.js a scalability disaster. The example shown for proof was a CPU-bound implementation of the Fibonacci sequence algorithm. While the argument was flawed—since nobody implements Fibonacci that way—it made the valid point that Node.js application developers have to consider the following: where do you put the heavy computational tasks?

A key to maintaining high throughput of Node.js applications is by ensuring that events are handled quickly. Because it uses a single execution thread, if that thread is bogged down with a big calculation, Node.js cannot handle events, and event throughput will suffer.

The Fibonacci sequence, serving as a stand-in for heavy computational tasks, quickly becomes computationally expensive to calculate for a naïve implementation such as this:

const fibonacci = exports.fibonacci = function(n) { 
   if (n === 1 || n === 2) {
return 1;
} else {
return fibonacci(n-1) + fibonacci(n-2);
} }

This is a particularly simplistic approach to calculating Fibonacci numbers. Yes, there are many ways to calculate Fibonacci numbers more quickly. We are showing this as a general example of what happens to Node.js when event handlers are slow and not to debate the best ways to calculate mathematical functions. Consider the following server:

const http = require('http'); 
const url  = require('url'); 
 
http.createServer(function (req, res) { 
  const urlP = url.parse(req.url, true); 
  let fibo; 
  res.writeHead(200, {'Content-Type': 'text/plain'}); 
  if (urlP.query['n']) { 
    fibo = fibonacci(urlP.query['n']);  // Blocking
    res.end('Fibonacci '+ urlP.query['n'] +'='+ fibo); 
  } else { 
    res.end('USAGE: http://127.0.0.1:8124?n=## where ## 
is the Fibonacci number desired'); } }).listen(8124, '127.0.0.1'); console.log('Server running at http://127.0.0.1:8124');

This is an extension of the simple web server shown earlier. It looks in the request URL for an argument, n, for which to calculate the Fibonacci number. When it's calculated, the result is returned to the caller.

For sufficiently large values of n (for example, 40), the server becomes completely unresponsive because the event loop is not running. Instead, this function has blocked event processing because the event loop cannot dispatch events while the function is grinding through the calculation.

In other words, the Fibonacci function is a stand-in for any blocking operation.

Does this mean that Node.js is a flawed platform? No, it just means that the programmer must take care to identify code with long-running computations and develop solutions. These include rewriting the algorithm to work with the event loop, rewriting the algorithm for efficiency, integrating a native code library, or foisting computationally expensive calculations to a backend server.

A simple rewrite dispatches the computations through the event loop, letting the server continue to handle requests on the event loop. Using callbacks and closures (anonymous functions), we're able to maintain asynchronous I/O and concurrency promises, as shown in the following code:

const fibonacciAsync = function(n, done) { 
if (n === 0) {
return 0;
} else if (n === 1 || n === 2) {
done(1);
} else if (n === 3) {
return 2; } else { process.nextTick(function() { fibonacciAsync(n-1, function(val1) { process.nextTick(function() { fibonacciAsync(n-2, function(val2) {
done(val1+val2); }); }); }); }); } }

This is an equally silly way to calculate Fibonacci numbers, but by using process.nextTick, the event loop has an opportunity to execute.

Because this is an asynchronous function that takes a callback function, it necessitates a small refactoring of the server:

const http = require('http'); 
const url  = require('url'); 
 
http.createServer(function (req, res) { 
  let urlP = url.parse(req.url, true);
  res.writeHead(200, {'Content-Type': 'text/plain'}); 
  if (urlP.query['n']) { 
    fibonacciAsync(urlP.query['n'], fibo => {  // Asynchronous
res.end('Fibonacci '+ urlP.query['n'] +'='+ fibo);
});
} else {
res.end('USAGE: http://127.0.0.1:8124?n=## where ## is the
Fibonacci number desired');
}
}).listen(8124, '127.0.0.1'); console.log('Server running at http://127.0.0.1:8124');

We've added a callback function to receive the result. In this case, the server is able to handle multiple Fibonacci number requests. But there is still a performance issue because of the inefficient algorithm.

Later in this book, we'll explore this example a little more deeply to explore alternative approaches.

In the meantime, we can discuss why it's important to use efficient software stacks.

Server utilization, overhead costs, and environmental impact

The striving for optimal efficiency (handling more requests per second) is not just about the geeky satisfaction that comes from optimization. There are real business and environmental benefits. Handling more requests per second, as Node.js servers can do, means the difference between buying lots of servers and buying only a few servers. Node.js potentially lets your organization do more with less.

Roughly speaking, the more servers you buy, the greater the monetary cost and the greater the environmental cost. There's a whole field of expertise around reducing costs and the environmental impact of running web-server facilities to which that rough guideline doesn't do justice. The goal is fairly obvious—fewer servers, lower costs, and a lower environmental impact by using more efficient software.

Intel's paper, Increasing Data Center Efficiency with Server Power Measurements (https://www.intel.com/content/dam/doc/white-paper/intel-it-data-center-efficiency-server-power-paper.pdf), gives an objective framework for understanding efficiency and data center costs. There are many factors, such as buildings, cooling systems, and computer system designs. Efficient building design, efficient cooling systems, and efficient computer systems (data center efficiency, data center density, and storage density) can lower costs and environmental impact. But you can destroy these gains by deploying an inefficient software stack, compelling you to buy more servers than you would if you had an efficient software stack. Alternatively, you can amplify gains from data center efficiency with an efficient software stack that lets you decrease the number of servers required.

This talk about efficient software stacks isn't just for altruistic environmental purposes. This is one of those cases where being green can help your business bottom line.

In this section, we have learned a lot about how Node.js architecture differs from other programming platforms. The choice to eschew threads to implement concurrency simplifies away the complexity and overhead that comes from using threads. This seems to have fulfilled the promise of being more efficient. Efficiency has a number of benefits to many aspects of a business.

 

Embracing advances in the JavaScript language

The last couple of years have been an exciting time for JavaScript programmers. The TC-39 committee that oversees the ECMAScript standard has added many new features, some of which are syntactic sugar, but several of which have propelled us into a whole new era of JavaScript programming. By itself, the async/await feature promises us a way out of what's called callback fell, the situation that we find ourselves in when nesting callbacks within callbacks. It's such an important feature that it should necessitate a broad rethinking of the prevailing callback-oriented paradigm in Node.js and the rest of the JavaScript ecosystem.

A few pages ago, you saw this:

query('SELECT * from db.table', function (err, result) { 
    if (err) throw err; // handle errors 
    // operate on result 
});

This was an important insight on Ryan Dahl's part, and is what propelled Node.js's popularity. Certain actions take a long time to run, such as database queries, and should not be treated the same as operations that quickly retrieve data from memory. Because of the nature of the JavaScript language, Node.js had to express this asynchronous coding construct in an unnatural way. The results do not appear at the next line of code, but instead appear within this callback function. Furthermore, errors have to be handled in an unnatural way, inside that callback function.

The convention in Node.js is that the first parameter to a callback function is an error indicator and the subsequent parameters are the results. This is a useful convention that you'll find all across the Node.js landscape; however, it complicates working with results and errors because both land in an inconvenient location—that callback function. The natural place for errors and results to land is on the subsequent line(s) of code.

We descend further into callback hell with each layer of callback function nesting. The seventh layer of callback nesting is more complex than the sixth layer of callback nesting. Why? If nothing else, it's because the special considerations for error handling become ever more complex as callbacks are nested more deeply.

But as we saw earlier, this is the new preferred way to write asynchronous code in Node.js:

const results = await query('SELECT * from db.table');

Instead, ES2017 async functions return us to this very natural expression of programming intent. Results and errors land in the correct location while preserving the excellent event-driven asynchronous programming model that made Node.js great. We'll see how this works later in the book.

The TC-39 committee added many more new features to JavaScript, such as the following:

  • An improved syntax for class declarations, making object inheritance and getter/setter functions very natural.
  • A new module format that is standardized across browsers and Node.js.
  • New methods for strings, such as the template string notation.
  • New methods for collections and arrays—for example, operations for map/reduce/filter.
  • The const keyword to define variables that cannot be changed and the let keyword to define variables whose scope is limited to the block in which they're declared, rather than hoisted to the front of the function.
  • New looping constructs and an iteration protocol that works with those new loops.
  • A new kind of function, the arrow function, which is lighter in weight, meaning less memory and execution time impact.
  • The Promise object represents a result that is promised to be delivered in the future. By themselves, promises can mitigate the callback hell problem, and they form part of the basis for async functions.
  • Generator functions are an intriguing way to represent asynchronous iteration over a set of values. More importantly, they form the other half of the basis for async functions.

You may see the new JavaScript described as ES6 or ES2017. What's the preferred name to describe the version of JavaScript that is being used?

ES1 through ES5 marked various phases of JavaScript's development. ES5 was released in 2009 and is widely implemented in modern browsers. Starting with ES6, the TC-39 committee decided to change the naming convention because of their intention to add new language features every year. Therefore, the language version name now includes the year—for example, ES2015 was released in 2015, ES2016 was released in 2016, and ES2017 was released in 2017.

Deploying ES2015/2016/2017/2018 JavaScript code

The elephant in the room is that often JavaScript developers are unable to use the latest features. Frontend JavaScript developers are limited by the deployed web browsers and the large number of old browsers in use on machines whose OS hasn't been updated for years. Internet Explorer version 6 has fortunately been almost completely retired, but there are still plenty of old browsers installed on older computers that are still serving a valid role for their owners. Old browsers mean old JavaScript implementations, and if we want our code to work, we need it to be compatible with old browsers.

One of the uses for Babel and other code-rewriting tools is to deal with this issue. Many products must be usable by folks using an old browser. Developers can still write their code with the latest JavaScript or TypeScript features, then use Babel to rewrite their code so that it runs on the old browser. This way, frontend JavaScript programmers can adopt (some of) the new features at the cost of a more complex build toolchain and the risk of bugs being introduced by the code-rewriting process.

The Node.js world doesn't have this problem. Node.js has rapidly adopted ES2015/2016/2017 features as quickly as they were implemented in the V8 engine. Starting with Node.js 8, we were able to freely use async functions as a native feature. The new module format was first supported in Node.js version 10.

In other words, while frontend JavaScript programmers can argue that they must wait a couple of years before adopting ES2015/2016/2017 features, Node.js programmers have no need to wait. We can simply use the new features without needing any code-rewriting tools, unless our managers insist on supporting older Node.js releases that predate the adoption of these features. In that case, it is recommended that you use Babel.

Some advances in the JavaScript world are happening outside the TC-39 community.

TypeScript and Node.js

The TypeScript language is an interesting offshoot of the JavaScript environment. Because JavaScript is increasingly able to be used for complex applications, it is increasingly useful for the compiler to help catch programming errors. Enterprise programmers in other languages, such as Java, are accustomed to strong type checking as a way of preventing certain classes of bugs.

Strong type checking is somewhat anathema to JavaScript programmers, but is demonstrably useful. The TypeScript project aims to bring enough rigor from languages such as Java and C# while leaving enough of the looseness that makes JavaScript so popular. The result is compile-time type checking without the heavy baggage carried by programmers in some other languages.

While we won't use TypeScript in this book, its toolchain is very easy to adopt in Node.js applications.

In this section, we've learned that as the JavaScript language changes, the Node.js platform has kept up with those changes.

 

Developing microservices or maxiservices with Node.js

New capabilities, such as cloud deployment systems and Docker, make it possible to implement a new kind of service architecture. Docker makes it possible to define server process configuration in a repeatable container that's easy to deploy by the millions into a cloud-hosting system. It lends itself best to small, single-purpose service instances that can be connected together to make a complete system. Docker isn't the only tool to help simplify cloud deployments; however, its features are well attuned to modern application deployment needs.

Some have popularized the microservice concept as a way to describe this kind of system. According to the microservices.io website, a microservice consists of a set of narrowly focused, independently deployable services. They contrast this with the monolithic application-deployment pattern where every aspect of the system is integrated into one bundle (such as a single WAR file for a Java EE app server). The microservice model gives developers much-needed flexibility.

Some advantages of microservices are as follows:

  • Each microservice can be managed by a small team.
  • Each team can work on its own schedule, so long as the service API compatibility is maintained.
  • Microservices can be deployed independently should this be required, such as for easier testing.
  • It's easier to switch technology stack choices.

Where does Node.js fit in with this? Its design fits the microservice model like a glove:

  • Node.js encourages small, tightly focused, single-purpose modules.
  • These modules are composed into an application by the excellent npm package management system.
  • Publishing modules is incredibly simple, whether via the NPM repository or a Git URL.
  • While an app framework such as Express can be used with large services, it works very well for small lightweight services and supports easy, simple deployment.

In short, it's easy to use Node.js in a lean and agile fashion, building large or small services depending on your architecture preferences.

 

Summary

You learned a lot in this chapter. Specifically, you saw that JavaScript has a life outside web browsers and that Node.js is an excellent programming platform with many interesting attributes. While it is a relatively young project, Node.js has become very popular and is widely used not just for web applications but for command-line developer tools and much more. Because the Node.js platform is based on Chrome's V8 JavaScript engine, the project has been able to keep up with the rapid improvements to the JavaScript language.

The Node.js architecture consists of asynchronous functions managed by an event loop triggering callback functions, rather than using threads and blocking I/O. This architecture has claimed performance benefits that seem to offer many benefits, including the ability to do more work with less hardware. But we also learned that inefficient algorithms can erase any performance benefits.

Our focus in this book is the real-world considerations of developing and deploying Node.js applications. We'll cover as many aspects of developing, refining, testing, and deploying Node.js applications as we can.

Now that we've had this introduction to Node.js, we're ready to dive in and start using it. In Chapter 2, Setting up Node.js, we'll go over how to set up a Node.js development environment on Mac, Linux, or Windows, and even write some code. So let's get started.

About the Author

  • David Herron

    David Herron is a software engineer living in Silicon Valley who has worked on projects ranging from an X.400 email server to being part of the team that launched the OpenJDK project, to Yahoo's Node.js application-hosting platform, and a solar array performance monitoring service. That took David through several companies until he grew tired of communicating primarily with machines, and developed a longing for human communication. Today, David is an independent writer of books and blog posts covering topics related to technology, programming, electric vehicles, and clean energy technologies.

    Browse publications by this author

Latest Reviews

(2 reviews total)
The is very constructive and great
Once again I have bought the previous versions of this book and it is excellent

Recommended For You

Node.js Web Development - Fifth Edition
Unlock this book and the full library for $5 a month*
Start now