Skip to content

Latest commit

 

History

History
648 lines (438 loc) · 41.6 KB

ch01.adoc

File metadata and controls

648 lines (438 loc) · 41.6 KB

Node Fundamentals

Node is an open-source cross-platform runtime environment that can be used to execute JavaScript code using the same JavaScript engine in the Chrome web browser. Node has dozens of built-in modules which offer asynchronous APIs that make it easy for developers to build and maintain efficient backend services without the complexity of dealing with multiple processes and threads.

There’s a lot to unpack here and that’s what we will be doing in this first chapter. We’ll start with an introduction to Node, what it is, how it works, and why it’s a very popular option for both backend and frontend development. We’ll learn how to set up a Node development environment, and execute a Node script. We’ll see examples of utilizing the built-in modules within Node, and demonstrate how to install and use a non-built-in library as well.

Note

Throughout the book, I use the term Node instead of Node.js for brevity. The official name of the runtime environment is Node.js but referring to it as just Node is common. Don’t confuse that with node (with a lower-case n), which is the command to execute a Node script.

Introduction

Ryan Dahl started Node in 2009 after he was inspired by the performance of the V8 JavaScript engine. V8 uses an event-driven model, which makes it efficient at handling concurrent connections and requests. Ryan wanted to bring this same high-performance, event-driven architecture to server-side applications. The event-driven model is the first and most important concept you need to understand about Node (and the V8 engine as well). I’ll explain it briefly in this section and we’ll expand on it in Chapter 3.

Tip

I decided to give Node a spin and learn more about it after watching the presentation Ryan Dahl gave to introduce it. I think you’ll benefit by starting there as well. Search YouTube for "Ryan Dahl introduction to Node". Node has changed significantly since then, so don’t focus on the examples, but rather the concepts and explanations.

In its core, Node enables developers to use the JavaScript language on any machine without needing a web browser. Node is usually defined as JavaScript on backend servers. Before Node, that was not a common or easy thing. JavaScript was mainly a frontend thing.

However, this definition isn’t a completely accurate one. Node offers a lot more than executing JavaScript on servers. In fact, the actual execution of JavaScript is done by the V8 JavaScript engine, not Node. Node is just an interface to V8 when it comes to executing JavaScript code.

V8 is Google’s open source JavaScript engine that’s written in C++. It parses and executes JavaScript code. V8 is used in Node, Chrome, and a few other browsers. It’s also used in Deno, the new JavaScript runtime that was created by Ryan Dahl in 2018.

Note

There are other JavaScript engines like SpiderMonkey which is used by Firefox, JavaScriptCore which is used by the Safari web browser and in Bun, an all-in-one JavaScript runtime, package manager, and bundler.

Node is better defined as a server runtime environment that wraps V8 and provides modules to help developers build and run efficient software applications with JavaScript.

The key word in this definition is efficient. Node adopts and expands the same event-driven model that V8 has. Most of Node’s built-in modules APIs are event-driven and can be used asynchronously without blocking the main thread of execution that your code runs in.

We’ll expand on this very important concept once we learn the basics of running Node code and using its modules and packages.

Executing Node Code

If you have Node installed on your computer, you should have the commands node and npm available in a terminal. If you have these commands, make sure the Node version is a recent one (20.x or higher). You can verify that by opening a terminal and running the command node -v (and npm -v).

If you don’t have these commands at all, you’ll need to download and install Node. You can download the latest version from the official Node website (https://nodejs.org/). The installation process is straightforward and should only take a few minutes.

For MacOS users, Node can also be installed using the Homebrew package manager with the command:

$ brew install node
Note

Throughout this book, I use the $ sign to indicate a command line to be executed in a terminal. The $ sign is not part of the command. It’s a common prompt character in terminals.

Another option to install Node is using Node Version Manager (NVM). NVM allows you to run multiple versions of Node and switch between them easily. You might need to run a certain project with an older version of Node, and use the latest Node version with another project. NVM works on Mac and Linux, and there’s an NVM-windows option as well.

Node on Windows

All the examples I will be using in this book are designed for a MacOS environment and should also work for a Linux-based OS. On Windows, you need to switch the commands I use with their Windows alternatives.

I don’t recommend using Node on Windows natively unless it’s your only option. If you have a modern Windows machine, one option that might work a lot better for you is to install the Windows subsystem for Linux. This option will give you the best of both worlds. You’ll have your Windows operating system running Linux without needing to reboot. You can even edit your code in a Windows editor, and execute it on Linux!

If you’re using NVM, install the latest version of Node with the command:

$ nvm install node
Tip

Major Node versions are released frequently. When a new version is released, it enters a Current release status for six months to give library authors time to make their libraries compatible with the new version. After six months, odd-numbered releases (19, 21, etc) become unsupported, and even-numbered releases (18, 20, etc) move to Active LTS status (Long Term Support). LTS release typically guarantees that critical bugs will be fixed for a total of 30 months. Production applications should only use active LTS releases.

Once you have the node command ready, open a terminal and issue the command on its own without any arguments. This will start a Node REPL session. REPL stands for Read, Eval, Print, Loop. It’s a convenient way to quickly test simple JavaScript and Node code. You can type any JavaScript code in a REPL session. For example, type Math.random() and then, press Enter:

node repl
Figure 1. Node’s REPL mode

Node will read the line, evaluate it, print the result, and loop over these three things for everything you type until you exit the session (which you can do with a CTRL+D).

Note

We’ll learn more about Node’s REPL mode in Chapter 2.

Note how the Print step happened automatically. We didn’t need to add any instructions to print the result. Node will just print the result of each line you type. This is not the case when you execute code in a Node script. Let’s do that next.

Create a new directory for the exercises of this book, and then cd into it:

$ mkdir efficient-node
$ cd efficient-node

Open up your editor for this directory, then create a file named test.js. Put the same Math.random() line into it:

Math.random();

Now to execute that file, in the terminal, type the command:

$ node test.js

You’ll notice that the command will basically do nothing. That’s because we have not outputted anything from that file. To output something, you can use the global console object, which is similar to the one available in browsers. Put the following code in test.js:

console.log(
  Math.random()
);

Executing test.js now will output a random number:

node test console
Figure 2. Executing a Node script

Note how in this simple example we’re using both JavaScript (Math object), and an object from the Node API (console). Let’s look at a more interesting example next.

Note

The console object is one of many top-level global scope objects that we can access in Node without needing to declare any dependencies. Node has a global object similar to the window object in browsers. The globalThis property is a standard way of accessing the this value in the global scope. The console object is part of the global scope. All properties of globalThis can be accessed directly; console.log instead of globalThis.console.log (which also works).

Using a Built-in Module

You can create a simple web server in Node using its built-in node:http module.

Create a server.js file and write the following code in there:

// Basic Web Server Example

const { createServer } = require('node:http');

const server = createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Hello World');
});

server.listen(3000, '127.0.0.1', () => {
  console.log('Server is running...');
});

This is Node’s version of a Hello World example. You don’t need to install anything to run this script. This is all Node’s built-in power.

When you execute this script:

$ node server.js

Node will run a web server, and you’ll notice that the Node process does not exit in this example. It has work to do in the background. It waits for users to request data and serve them a plain "Hello World" text when they do.

node basic web server
Figure 3. Basic web server example

While this web server example is a basic one, it has a few important concepts to understand. Let’s go over it in detail.

The require function (on the first line) is part of Node’s original dependency management method. It allows a module (like server.js) to load and use the API of another module (like node:http). This web server module depends on the built-in node:http module. There are many other libraries that you can use to create a web server, but this one is built-in. You don’t need to install anything to use it, but you do need to require it.

Tip

In a Node’s REPL session, built-in modules (like node:http) are available immediately without needing to require them. This is not the case with executable scripts. You can’t use modules (including built-in ones) without requiring them first.

The second line creates a server object by invoking the createServer function from the node:http module. This function is one of many functions that are available under the node:http module’s API. You can use it to create a web server object. It accepts an argument that is known as the Request Listener.

A listener function in Node is associated with a certain event and it gets executed when its event is triggered. In this example, Node will execute the request listener function every time there is an incoming connection request to the web server. That’s the event associated with this listener function.

The listener function receives two arguments:

  • The request object, named req in this example. You can use this object to read data about incoming requests. For example, what URL is being requested, or what is the IP address of the client that’s making a request.

  • The response object, named res in this example. you can use this object to write things back to the requester. It’s exactly what this simple web server is doing. It’s setting the response status code to 200 to indicate a successful response, and the Content-Type header to text/plain. Then it’s writing back the Hello World text using the end method on the res object.

Note

The .end method can be used as a shortcut to write data and then finalize the response in one line.

The createServer function only creates the server object. It does not activate it. To activate this web server, you need to invoke the listen method on the created server.

The listen method accepts many arguments, like what OS port and host to use for this server. The last argument for it is a function that will be invoked once the server is successfully running on the specified port. This example prints a console message to indicate that the server is running successfully at that point.

While the server is running, if you go to a browser and ask for an http connection on localhost with the port that was used in the script (3000 in this case), you will see the Hello World text that this example had in its request listener function. To stop the web server, press CTRL+C in the terminal where it’s running.

Both functions received by the createServer and listen methods are examples of functions associated with events related to an asynchronous operation. Node manages these event functions with an event queue and a forever ticking event loop. The event loop is a very important architectural concept to understand in Node. We’ll learn all about Node asynchrony, events, and the event loop in Chapter 3.

Tip

Note how I use a node: prefix when working with the built-in modules in Node. This is a helpful practice to distinguish them from external modules and identify their built-in nature immediately. Since it’s required for a few modules (like node:test), it’s good to be consistent and use it for all modules.

Using an npm Package

npm is Node’s Package Manager. It’s a simple CLI (Command Line Interface) that lets us install and manage external packages in a Node project. An npm package can be a single module or a collection of modules grouped together and exposed with an API. We’ll talk more about npm and its commands and packages in Chapter 5. Here, let’s just look at a simple example of how to install and use an npm package.

Let’s use the popular lodash package which is a JavaScript utility library with many useful methods you can run on numbers, strings, arrays, objects, and more.

First, you need to download the package. You can do that using the npm install command:

$ npm install lodash

This command will download the lodash package from the npm registry, and then place it under a node_modules folder (which it will create if it’s not there already). You can verify with an ls command:

$ ls node_modules

You should have a folder named lodash in there.

Now in your Node code, you can require the lodash module to use it. For example, lodash has a random method that can generate a random number between any two numbers you pass to it. Here’s an example of how to use it:

const _ = require('lodash');

console.log(
  _.random(1, 99)
);

When you run this script, you’ll get a random number between 1 and 99.

node use lodash
Figure 4. Using an npm package
Tip

The _ is a common name to use for lodash, but you can use any name.

Since we called the require method with a non built-in module lodash, Node will look for it under the node_modules folder. Thanks to npm, it’ll find it.

In a team Node project, when you make the project depend on an external package like this, you need to let other developers know of that dependency. You can do so in Node using a package.json file at the root of the project.

When you npm install a module, the npm command will also list the module and its current version in package.json, under a dependencies section. Look at the package.json file that was auto created when you installed the lodash package and see how the lodash dependency was documented.

node packagejson auto
Figure 5. The package.json file

The package.json file can also contain information about the project, including the project’s name, version, description, and more. It can also be used to specify scripts that can be run from the command line to perform various tasks, like building or testing the project.

Here’s an example of a typical package.json file:

{
  "name": "efficient-node",
  "version": "1.0.0",
  "description": "A guide to learning Node.js",
  "license": "MIT",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "lodash": "^4.17.21"
  }
}

You can interactively create a package.json file for a new Node project using the npm init command:

$ npm init

This command will ask a few questions and you can interactively supply your answers (or press Enter to keep the defaults, which often are good because the command tries to detect what it can about the project).

Try to npm install new packages (for example, chalk) and see how it gets listed as a dependency in your package.json file. Then npm uninstall the package and see how it gets removed from package.json.

Your package.json file will eventually list many dependencies. When other developers pull your code, they can run the command npm install without any arguments, and npm will read all the dependencies from package.json and install them under the node_modules folder.

Some packages are only needed in a development environment, but not in a production environment. An example of that is eslint and it’s one of my favorite code quality tools (we’ll learn all about it in Chapter 10). You can instruct the npm install command to list a package as a development-only dependency by adding the --save-dev argument (or -D for short):

$ npm install -D eslint

This will install the eslint package in the node_modules folder, and document it as a development dependency under a devDependencies section in package.json. This is where you should place things like your testing framework, your formatting tools, or anything else that you use only while developing your project.

Tip

In addition to dependencies and devDependencies, a package.json file can also specify optionalDependencies for packages that are optional, and peerDependencies for packages that need to work alongside other packages but do not directly depend on them. Peer dependencies are only needed by package authors.

If you take a look at what’s under node_modules after you install eslint, you’ll notice that there are a lot more packages there.

npm install
Figure 6. npm packages and their indirect dependencies

The eslint package depends on all these other packages. Be aware of these inderict dependencies in the future. By depending on one package, a project is indirectly depending on all that package’s dependencies, and the dependencies of all the sub dependencies, and so on. With every package you install, you add a tree of dependencies.

Some packages can also be installed (and configured) directly with the init command. ESLint is an example of a package that needs a configuration file before you can use it. The following command will install ESLint and create a configuration file for it (after asking you a few questions about your project):

$ npm init @eslint/config@latest
Tip

In a production machine, development dependencies are usually ignored. The npm install command has a --production flag to make it ignore them. You can also use the NODE_ENV environment variable and set it to production before you run the npm install command. We’ll learn more about Node environments and variables in Chapter 2.

Using ES Modules

So far, we’ve been using Node`s require method to declare a dependency in a module. This method is part of Node’s CommonJS module system, which is the default module system used in Node.

Node also supports ES Modules, the modern ECMAScript standard for working with modules in JavaScript. ES modules are supported in modern browsers. They use import and export statements to define dependencies and expose functionality between different modules.

One important difference between these two module systems in Node, is that CommonJS modules get loaded dynamically at runtime. ES Module dependencies are determined at compile time, allowing them to be statically analyzed and optimized. For example, with ES modules, we can easily find what code is not being used, and exclude it from the compiled version of the application.

Tip

While the two module types can be used together, you need to be careful about mixing them. CommonJS modules are fully synchronous while ES modules are asynchronous.

To see ES modules in action, let’s expand on the basic web server example code and split it into two modules, one to create the web server, and one to run it.

The simplest way to use ES modules in Node is to save files with a .mjs file extension instead of a .js extension. This is because by default, Node assumes that all .js extensions are using the CommonJS module system. This is configurable though. To make Node treat all .js files as ES modules, you can add a type key in package.json and give it the value of module (the default value for it is commonjs):

  "type": "module"
Tip

npm has a pkg command that you can use to manage a package.json file. For example, you can use npm pkg set type=module to add the type key with a module value.

With that, you can now use ES modules with the .js extension.

Tip

Regardless of what default module type you use, Node will always assume a .mjs file is an ES module file, and a .cjs file is a CommonJS module file. You can import a .cjs file into an ES module, and you can import a .mjs file into a CommonJS module.

Let’s modify the basic web server example to use ES modules. In server.js file, write the following code:

import http from 'node:http';

export const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Hello World');
});

Note the use of import/export statements. This is the syntax for ES modules. You use import to declare a module dependency and export to define what other modules can use when they depend on your module.

In this example, the server.js module exports the server object, enabling other modules to import it and depend on it.

To use the server.js module, just like we imported node:http in there, we import the server.js module into other modules. In index.js file, write the following code:

import { server } from './server.js';

server.listen(3000, () => {
  console.log('Server is running...');
});

The ./ part in the import line signals to Node that this import is a relative one. Node expects to find the server.js file in the same folder where index.js is. You can also use a ../ to make Node look for the module up one level, or ../../ for two levels, and so on. Without ./ or ../, Node assumes that the module you’re trying to import is either a built-in module, or a module that exists under the node_modules folder.

With this code, the index.js module depends on the server.js module, and uses its exported server object to run the server on port 3000. Execute the index.js file with the node command to start the web server and test it.

node esm http
Figure 7. ES modules web server example

The export object syntax is known as named exports and it’s great when you need to export multiple things in a module. You can use the export keyword to prefix any object, including functions, classes, destructured variables, etc:

export function functionName() { ... }
export class ClassName { ... }
export const [name1, name2]

You can also use one export keyword, usually at the end of a module, with one object to export all named object together:

export {
  server,
  functionName,
  ClassName,
  name1,
  name2,
  // ...
};

In addition to the named export syntax, ES modules also have a default export syntax:

// To export the server object
// as the default export in server.js:

export default server;

// To import it, you need to
// specify a name:

import myServer from "./server.js";

Note how to import a default export, you have to name it, while with named exports, you don’t (although you can if you need to). Named exports are better for consistency, discoverability, and maintainability.

The export/import keywords support other syntax like renaming and exporting from other modules, but in general, my recommendation is to avoid using default exports, always use named exports and keep them simple and consistent. For example, I always specify my named exports as the last line of the module with a single export { …​ } statement.

Note

ES modules are the modern standard for JavaScript modules and the preferred module system to use in Node today. CommonJS modules will continue to work for legacy and compatibility reasons. Many Node projects and libraries are built using CommonJS modules and it’s very likely that you will have to deal with them even if you’re starting a project from scratch.

An Analogy for Node and npm

Real-life analogies can sometimes help us understand coding concepts.

One of my favorite analogies about coding in general is how it can be compared to writing cooking recipes. The recipe in this analogy is the program, and the cook is the computer.

In some recipes, you can use pre-made items like a cake mix or a special sauce. You would also need to use tools like a pan or a strainer. When compared to coding, you can think of these pre-made items and tools as the packages of code written by others which you can just download and use.

Extending on this analogy, you can think of an npm registry as the store where you get your pre-made items and tools for your coding recipes.

But what exactly is Node’s place in this analogy?

I like to think of it as the kitchen! it allows you to execute lines in your coding recipes by using built-in tools, like the stove and the sink.

Now imagine trying to cook without these built-in tools in your kitchen. That would make the task a lot harder, wouldn’t it?

While static import declarations are preferable for loading initial dependencies, there are many cases where you will need to import modules dynamically. For example:

  • When a module is slowing the loading of your code and it’s not needed until a later time.

  • When a module does not exist at load time.

  • When you need to dynamically construct the name of the module to import.

  • When you need to import a module conditionally.

For these cases, we can use the import() function. It’s similar to the require() function but it’s an asynchronous one.

Let’s think about an example where we need to read the content of a file before starting the basic web server. We can simulate the file reading delay using a simple setTimeout function.

Since we don’t need the server.js module until a later point in time, we can import it with the import() function when we are ready for it:

setTimeout(async () => {
  const { server } = await import('./server.js');

  server.listen(3000, () => {
    console.log('Server is running...');
  });
}, 5_000);

If you execute this code, the Node process will wait 5 seconds, it’ll then dynamically import the server.js module and use it to start the server. We’ll learn more timer objects in Chapter 3.

Tip

Dynamic import() expressions can be used in CommonJS modules to load ES modules.

This example introduces the important Promise Object concept and how to consume it with async/await. This is the modern JavaScript syntax to handle asynchronous operations and that’s the first important Node fundamental concept that we need to understand. Let’s do that next.

JavaScript and the Non-blocking Model

After considering programming languages like Python, Lua, and Haskell, Ryan Dahl picked the JavaScript language for Node because it was a great fit. It’s simple, flexible, and popular, but more importantly, JavaScript functions are first-class citizens that you can pass around while preserving their state. Node leveraged that to implement its simple callback pattern for asynchronous operations, which is also known as the non-blocking model.

Note

Despite JavaScript’s historical problems, I believe it’s a decent language today that can be made even better by using TypeScript (which we will discuss in Chapter 10).

When you need to perform a slow operation in your code (like reading a file from the file system), you’ll need a way to handle the output of that slow operation.

const output = slowOperation();
handlerFunction(output)

// Other operations

The problem with this is that the slow operation will block the execution for all other operations that follow it, but since JavaScript functions can be passed as arguments, we can design the slowOperation function to invoke its handler function once it’s done, using the following pattern:

slowOperation(
  (output) => handlerFunction(output)
);

// Other operations

Now, we can make the slowOperation run in a different process (or subprocess) and the other operations in the main process will not be blocked. This is known as the callback pattern and it’s the original implementation of asynchrony in Node. The handlerFunction in this example is a callback function since it gets called at a later point in time once the slow operation is done.

A few years after the success of Node and its use of the callback pattern, promise objects were introduced in the JavaScript language and it became possible to natively wrap an asynchronous operation as a promise object to which handler functions can be attached:

const outputPromise = slowOperation();
outputPromise.then(
  (output) => handlerFunction(output)
);

// Other operations

Both of these patterns can be used in Node today to build on top of the asynchronous APIs of its built-in modules. We’ll see many examples of callbacks and promises and learn what happens under the hood to make them work in Chapter 3.

Besides simplifying the implementation of asynchronous operations, the fact that JavaScript is the programming language of browsers gave Node the advantage of having a single language across the full-stack. There are some subtle but great benefits to that:

  • One language means less syntax to keep in your head, less APIs and tools to work with, and less mistakes over all.

  • One language means better integrations between your frontend code and your backend code. You can actually share code between these two sides. For example, You can build a frontend application with a JavaScript framework like React, then use Node to render the same components of that frontend application on the server and generate initial HTML views for the frontend application. This is known as server-side rendering (SSR) and it’s something that many Node web frameworks offer out of the box.

  • One language means teams can share responsibilities among different projects. Projects don’t need a dedicated team for the frontend and a different team for the backend. You would also eliminate some dependencies between teams. A full-stack project can be assigned to a single team, The JavaScript People; they can develop APIs, they can develop web and network servers, they can develop interactive websites, and they can even develop mobile and desktop applications. Hiring JavaScript developers who can contribute to both frontend and backend applications is attractive to employers.

Node Built-in Modules

Armed with a simple non-blocking model, Ryan Dahl and many early contributors to Node got to work and implemented many low-level modules to offer asynchronous APIs for features like reading and writing files, sending and receiving data over network, compressing and encrypting data, and dozens of other features.

We saw simple examples of using the node:http module. To see the list of all built-in modules you get with Node, you can use this line (in a REPL session):

require("repl").builtinModules
node builtin modules
Figure 8. Node’s built-in modules

This is basically the list of things you need to learn to master Node.

Well, not all of it. Depending on the version of Node, this list might include deprecated (or soon to be deprecated) modules. You also might not need many of these modules depending on the scope of work and many other factors. For example, instead of using the native HTTPS capabilities of Node, you can simply put your Node HTTP server behind a reverse proxy like Nginx or a service like Cloudflare. Similarly, you would need to learn a module like wasi only if you’re working with Web Assembly.

Note how a few of these modules are included twice, one with a /promises suffix. These are the modules that support both the callback and the promise patterns.

Tip

Not all Node modules will be included in this list. Prefix-only modules and other experimental modules do not show up here. Examples include modules like node:test, node:sea, node:sqlite. For a full list of all modules and their development status, check out the stability overview table at nodejs.org/docs/latest/api/documentation.html

It’s good to get familiar with this list now, and get a taste of what you can do with Node. Here are some of the important modules with a description of the main tasks you can do with them:

  • node:assert: Verify invariants for testing

  • node:buffer: Represent and handle binary data

  • node:child_process: Run shell commands and fork processes

  • node:cluster: Scale a process by distributing its load across multiple workers

  • node:console: Output debugging information

  • node:crypto: Perform cryptographic functions

  • node:dns: Perform name resolutions like IP address lookup

  • node:events: Define custom events and handlers

  • node:fs: Interact with the file system

  • node:http: Create HTTP servers and clients

  • node:net: Create network servers and clients

  • node:os: Interact with the operation system

  • node:path: Handle paths for files and directories

  • node:perf_hooks: Measure and analyze applications performance

  • node:readline: Read data from readable streams one line at a time

  • node:stream: Handle large amounts of data efficiently

  • node:test: Create and run JavaScript tests

  • node:timers: Schedule code to be executed at a future time

  • node:url: Parse and resolve URL objects

  • node:util: Access useful utility functions

  • node:zlib: Compress and decompress data

We’ll see many examples of using these modules throughout the book. Some of these modules have entire chapters focusing on them.

Package and Dependency Management

Node ships with a powerful package manager named npm. We did not have a package manager in the JavaScript world before Node. npm was nothing short of revolutionary. It changed the way we work with JavaScript.

You can build many features in a Node application just by using code that’s freely available on npm. The npm registry has more than a million packages that you can just install and use in your Node servers. npm is a reliable package manager which comes with a simple CLI. The main npm command offers simple ways to install and maintain third-party packages, share your own code, and reuse it too.

Tip

You can install packages for Node from other package registries as well. For example, you can install them directly from GitHub.

Node also comes with the CommonJS module dependency manager and it supports ES modules as well. Node’s original module dependency management has been available since Node was released and it opened the door to a lot of flexibility in how we code JavaScript! It is widely used, even for JavaScript that gets executed in the browser, because npm has many tools to bridge the gap between modules written in Node and what browsers can work with today.

npm and Node’s module systems together make a big difference when you work with any JavaScript system, not just the JavaScript that you execute on backend servers or web browsers. For example, if you have a fancy fridge monitor that happens to run on JavaScript, you can use Node and npm for the tools to package, organize, and manage dependencies, and then bundle your code, and ship it to your fridge!

The packages that you can run on Node come in all shapes and forms, some are small and dedicated to specific programming tasks, some offer tools to assist in the life cycles of an application, others help developers every day to build and maintain big and complicated applications. Here are a few example of some of my favorite tools available from npm:

  • ESLint: A tool that you can include in any Node applications, and use it to find problems with your JavaScript code, and in some cases, automatically fix them. You can use ESLint to enforce best practices and consistent code style, but ESLint can help point out potential runtime bugs too. You don’t ship ESLint in your production environments, it’s just a tool that can help you increase the quality of your code as you write it.

  • Prettier: An opinionated code formatting tool. With Prettier, you don’t have to manually indent your code, break long code into multiple lines, remember to use a consistent style for the code (for example, always use single or double quotes, always use semicolons, never use semicolons, etc). Prettier automatically takes care of all that.

  • Webpack: A tool that assists with asset bundling. The Webpack Node package makes it very easy to bundle your multi-file frontend frameworks application into a single file for production and compile JavaScript extensions (like JSX for React) during that process. This is an example of a Node tool that you can use on its own. You do not need a Node web server to work with Webpack.

  • TypeScript: A tool that adds static typing and other features to the JavaScript language. It is useful because it can help developers catch errors before the code is run, making it easier to maintain and scale large codebases. TypeScript’s static typing can also improve developer productivity by providing better code auto-completion and documentation in development tools.

All of these tools (and many more) enrich the experience of creating and maintaining JavaScript applications, both on the frontend and the backend. Even if you choose not to host your frontend applications on Node, you can still use Node for its tools. For example, you can host your frontend application with another framework such as Ruby on Rails and use Node to build assets for the Rails Asset Pipeline.

We will learn more about these tools (and others) in Chapter 10.

Arguments Against Node

Node’s approach to handling code in an asynchronous and non-blocking manner is a unique model of thinking and reasoning about code. If you’ve never done it before, it will feel weird at first. You need time to get your head wrapped around this model and get used to it.

Node has a relatively small standard library. This means that developers need to rely on third-party modules to perform most big tasks. There is a large amount of third-party modules available for Node. You need to do some research to pick the most appropriate and efficient ones. Many of these modules are small, which means you’ll need to use multiple modules in a single project. It’s not uncommon for a Node project to use hundreds of third-party modules. While this can enhance maintainability and scalability, it also requires more management and oversight. As modules are regularly updated or abandoned, it becomes necessary to closely monitor and update all modules used within a project, replacing deprecated options and ensuring that your code is not vulnerable to any of the security threats these modules might introduce.

Tip

Smaller code is actually why Node is named Node! In Node, we build simple small single-process building blocks (nodes) that can be organized with good networking protocols, to have them communicate with each other and scale up to build large, distributed programs.

Additionally, Node is optimized for I/O and high-level programming tasks but it may not be the best choice for CPU-bound tasks, such as image and video processing, which require a lot of computational power. Because Node is single-threaded, meaning that it can only use one core of a CPU at a time, performing tasks that require a lot of CPU processing power might lead to performance bottlenecks. JavaScript itself is not the best language for high-performance computation, as it is less performant than languages like C++ or Rust.

Node also has a high rate of release and version updates, this can create the need for constant maintenance and updates of the codebase, which can be a disadvantage for long-term projects.

Finally, the language you use in Node, JavaScript, has one valid argument against it. It is a dynamically typed language, which means objects don’t have explicitly declared types at compile time and they can change during runtime. This is fine for small projects but for bigger ones, the lack of strong typing can lead to errors that are difficult to detect and debug and it generally makes the code harder to reason with and to maintain.

Summary

Node is a powerful framework for building network applications. wraps the V8 virtual machine to enable developers to execute JavaScript code in a simple way, and it is built on top of a simple event-driven, non-blocking model that makes it easy for developers to create efficient and scalable applications.

The built-in modules in Node provide a low-level framework on which developers can base their applications so that they don’t start from scratch. Node’s module system allows developers to organize their code into reusable modules. These modules can be imported and used in other parts of the application.

Node has a large and active community that has created many popular packages that can be easily integrated into Node projects. These packages can be found and downloaded from the npm registry.

In the next chapter, we’ll explore Node’s CLI and REPL mode and learn how Node loads and executes modules.