Node Cookbook - Fourth Edition

5 (1 reviews total)
By Bethany Griggs
    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. Chapter 2: Handling I/O

About this book

A key technology for building web applications and tooling, Node.js brings JavaScript to the server enabling full-stack development in a common language. This fourth edition of the Node Cookbook is updated with the latest Node.js features and the evolution of the Node.js framework ecosystems.

This practical guide will help you to get started with creating, debugging, and deploying your Node.js applications and cover solutions to common problems, along with tips to avoid pitfalls. You'll become familiar with the Node.js development model by learning how to handle files and build simple web applications and then explore established and emerging Node.js web frameworks such as Express.js and Fastify. As you advance, you'll discover techniques for detecting problems in your applications, handling security concerns, and deploying your applications to the cloud. This recipe-based guide will help you to easily navigate through various core topics of server-side web application development with Node.js.

By the end of this Node book, you'll be well-versed with core Node.js concepts and have gained the knowledge to start building performant and scalable Node.js applications.

Publication date:
November 2020
Publisher
Packt
Pages
512
ISBN
9781838558758

 

Chapter 2: Handling I/O

Prior to Node.js, JavaScript was predominantly used in the browser. Node.js brought JavaScript to the server and has enabled us to interact with the operating system with JavaScript. Today, Node.js is one of the most popular technologies for building server-side applications.

Node.js interacts with the operating system at a fundamental level: input and output. This chapter will explore the core APIs provided by Node.js that allow us to interact with the standard I/O, the file system, and the network stack.

This chapter will show you how to read and write files both synchronously and asynchronously. Node.js was built to handle asynchronous code and enable a non-blocking model. Understanding how to read and write asynchronous code is key learning, and it will show how to leverage the capabilities of Node.js.

We will also learn about the core modules provided by Node.js. We'll be focusing on the File System module, which enables you to interact with the file system and files.

This chapter will cover the following recipes:

  • Handling standard I/O
  • Managing files with fs module
  • Inspecting file metadata
  • Watching for file updates
  • Creating TCP server and client communication
 

Technical requirements

This chapter assumes that you have a recent version of Node.js 14 installed, a Terminal or shell, and an editor of your choice. The code for this chapter will be available on GitHub at https://github.com/PacktPublishing/Node.js-14-Cookbook in the Chapter02 directory.

 

Handling standard I/O

STDIN (standard in) refers to an input stream that a program can use to read input from a command shell or Terminal. Similarly, STDOUT (standard out) refers to the stream that is used to write the output. STDERR (standard error) is a separate stream to STDOUT that is typically reserved for outputting errors and diagnostic data.

In this recipe, we're going to learn how to handle input with STDIN, write output to STDOUT, and log errors to STDERR.

Getting ready

For this recipe, let's first create a single file named greeting.js. The program will ask for user input via STDIN, return a greeting via STDOUT, and log an error to STDERR when invalid input is provided. Let's create a directory to work in, too:

$ mkdir interfacing-with-io
$ cd interfacing-with-io
$ touch greeting.js

Now that we've set up our directory and file, we're ready to move on to the recipe steps.

How to do it

In this recipe, we're going to create a program that can read from STDIN and write to STDIN and STDERR:

  1. First, we need to tell the program to listen for user input. This can be done by adding the following lines to greeting.js:
    process.stdin.on("data", (data) => {
      // processing on each data event
    });
  2. We can run the file using the following command. Observe that the application does not exit because it is continuing to listen for process.stdin data events:
    $ node greeting.js
  3. Exit the program using CTRL + C.
  4. We can now tell the program what it should do each time it detects a data event. Add the following lines below the // processing on each data event comment:
      const name = data.toString().trim().toUpperCase();
      process.stdout.write(`Hello ${name}!`);
  5. You can now type some input in to your program, and it will return the greeting and your name in uppercase:
    $ node greeting.js $ Beth   Hello BETH
  6. We can now add a check for whether the input string is empty, and log to STDERR if it is. Change your file to the following:
    process.stdin.on("data", (data) => {
      const name = data.toString().trim().toUpperCase();
      if (name !== "") {
        process.stdout.write(`Hello ${name}!`);
      } else {
        process.stderr.write("Input was empty.");
      }
    });

Now we've created a program that can read from STDIN and write to STDIN and STDERR.

How it works

process.stdin, process.stdout, and process.stderr are all properties on the process object. A global process object provides the information and control of the Node.js process. For each of the I/O channels, they emit data events for every chunk of data received. In this recipe, we were running the program in interactive mode where each data chunk was determined by the newline character when you hit Enter in your shell. process.stdin.on("data", (data) => {...}); is what listens for these data events. Each data event returns a Buffer object. The Buffer object (typically named data) returns a binary representation of the input.

const name = data.toString() is what turns the Buffer object into a string. The trim() function removes the newline character that denoted the end of each input.

We write to STDOUT and STDERR using the respective properties on the process object (process.stdout.write, process.stderr.write).

During the recipe, we also used CTRL + C to exit the program in the shell. CTRL + C sends SIGINT, or signal interrupt, to the Node.js process. For more information about signal events, refer to the Node.js Process API documentation at https://nodejs.org/api/process.html#process_signal_events.

Important note

Console APIs: Under the hood, console.log and console.err are using process.stdout and process.stderr.

See also

 

Managing files with fs module

Node.js provides several core modules, including the fs module. fs stands for File System, and this module provides the APIs to interact with the file system.

In this recipe, we'll learn how to read, write, and edit files using the synchronous functions available in the fs module.

Getting ready

  1. Create another directory for this recipe:
    $ mkdir working-with-files
    $ cd working-with-files
  2. And now let's create a file to read. Run the following in your shell to create a file containing some simple text:
    $ echo Hello World! > hello.txt
  3. We'll also need a file for our program—create a file named readWriteSync.js:
    $ touch readWriteSync.js

    Important note

    touch is a command-line utility included in Unix-based operating systems that is used to update the access and modification date of a file or directory to the current time. However, when touch is run with no additional arguments on a non-existent file, it will create an empty file with that name. touch is a typical way of creating an empty file.

How to do it

In this recipe, we'll synchronously read the file named hello.txt, manipulate the contents of the file, and then update the file using synchronous functions provided by the fs module:

  1. We'll start by requiring the built-in modules fs and path. Add the following lines to readWriteSync.js:
    const fs = require("fs");
    const path = require("path");
  2. Now let's create a variable to store the file path of the hello.txt file that we created earlier:
    const filepath = path.join(process.cwd(), "hello.txt");
  3. We can now synchronously read the file contents using the readFileSync() function provided by the fs module. We'll also print the file contents to STDOUT using console.log():
    const contents = fs.readFileSync(filepath, "utf8");
    console.log("File Contents:", contents);
  4. Now, we can edit the content of the file—we will convert the lowercase text into uppercase:
    const upperContents = contents.toUpperCase();
  5. To update the file, we can use the writeFileSync() function. We'll also add a log statement afterward indicating that the file has been updated:
    fs.writeFileSync(filepath, upperContents);
    console.log("File updated.");
  6. Run your program with the following:
    $ node readWriteSync.js
    File Contents: Hello World!
    File updated.

You now have a program that, when run, will read the contents of hello.txt, convert the text content into uppercase, and update the file.

How it works

The first two lines require the necessary core modules for the program.

const fs = require("fs"); will import the core Node.js File System module. The API documentation for the Node.js File System module is available at https://nodejs.org/api/fs.html. The fs module provides APIs to interact with the file system using Node.js. Similarly, the core path module provides APIs for working with file and directory paths. The path module API documentation is available at https://nodejs.org/api/path.html.

Next, we defined a variable to store the file path of hello.txt using the path.join() function and process.cwd(). The path.join() function joins the path sections provided as parameters with the separator for the specific platform (for example, / on Unix and \ on Windows environments).

process.cwd() is a function on the global process object that returns the current directory of the Node.js process. In this program, it is expecting the hello.txt file to be in the same directory as the program.

Next, we read the file using the fs.readFileSync() function. We pass this function the file path to read and the encoding, "utf8". The encoding parameter is optional—when the parameter is omitted, the function will default to returning a Buffer object.

To perform manipulation of the file contents, we used the toUpperCase() function available on string objects.

Finally, we updated the file using the fs.writeFileSync() function. We passed the fs.writeFileSync() function two parameters. The first was the path to the file we wished to update, and the second parameter was the updated file contents.

Important note

Both the readFileSync() and writeFileSync() APIs are synchronous, which means that they will block/delay concurrent operations until the file read or write is completed. To avoid blocking, you'll want to use the asynchronous versions of these functions covered in the There's more section.

There's more

Throughout this recipe, we were operating on our files synchronously. However, Node.js was developed with a focus on enabling the non-blocking I/O model, therefore, in many (if not most) cases, you'll want your operations to be asynchronous.

Today, there are three notable ways to handle asynchronous code in Node.js—callbacks, Promises, and async/await syntax. The earliest versions of Node.js only supported the callback pattern. Promises were added to the JavaScript specification with ECMAScript 2015, known as ES6, and subsequently, support for Promises was added to Node.js. Following the addition of Promise support, async/await syntax support was also added to Node.js.

All currently supported versions of Node.js now support callbacks, Promises, and async/await syntax. Let's explore how we can work with files asynchronously using these techniques.

Working with files asynchronously

Asynchronous programming can enable some tasks or processing to continue while other operations are happening.

The program from the Managing files with fs module recipe was written using the synchronous functions available on the fs module:

const fs = require("fs");
const path = require("path");
const filepath = path.join(process.cwd(), "hello.txt");
const contents = fs.readFileSync(filepath, "utf8");
console.log("File Contents:", contents);
const upperContents = contents.toUpperCase();
fs.writeFileSync(filepath, upperContents);
console.log("File updated.");

This means that the program was blocked waiting for the readFileSync() and writeFileSync() operations to complete. This program can be rewritten to make use of the asynchronous APIs.

The asynchronous version of readFileSync() is readFile(). The general convention is that synchronous APIs will have the term "sync" appended to their name. The asynchronous function requires a callback function to be passed to it. The callback function contains the code that we want to be executed when the asynchronous function completes.

  1. The readFileSync() function in this recipe could be changed to use the asynchronous function with the following:
    const fs = require("fs");
    const path = require("path");
    const filepath = path.join(process.cwd(), "hello.txt");
    fs.readFile(filepath, "utf8", (err, contents) => {
      if (err) {
        return console.log(err);
      }
      console.log("File Contents:", contents);
      const upperContents = contents.toUpperCase();
      fs.writeFileSync(filepath, upperContents);
      console.log("File updated.");
    });

    Observe that all of the processing that is reliant on the file read needs to take place inside the callback function.

  2. The writeFileSync() function can also be replaced with the asynchronous function, writeFile():
    const fs = require("fs");
    const path = require("path");
    const filepath = path.join(process.cwd(), "hello.txt");
    fs.readFile(filepath, "utf8", (err, contents) => {
      if (err) {
        return console.log(err);
      }
      console.log("File Contents:", contents);
      const upperContents = contents.toUpperCase();
      fs.writeFile(filepath, upperContents, function (err) {
        if (err) throw err;
        console.log("File updated.");
      });
    });
  3. Note that we now have an asynchronous function that calls another asynchronous function. It's not recommended to have too many nested callbacks as it can negatively impact the readability of the code. Consider the following:
    first(args, () => {
        second(args, () => {
            third(args, () => {});
        });
    });
  4. There are approaches that can be taken to avoid callback hell. One approach would be to split the callbacks into named functions. For example, our file could be rewritten so that the writeFile() call is contained within its own named function, updateFile():
    const fs = require("fs");
    const path = require("path");
    const filepath = path.join(process.cwd(), "hello.txt");
    fs.readFile(filepath, "utf8", (err, contents) => {
      if (err) {
        return console.log(err);
      }
      console.log("File Contents:", contents);
      const upperContents = contents.toUpperCase();
      updateFile(filepath, upperContents);
    });
    function updateFile(filepath, contents) {
      fs.writeFile(filepath, contents, (err) => {
        if (err) throw err;
        console.log("File updated.");
      });
    }

    Another approach would be to use Promises, which we'll cover in the Using the fs Promise API section of this chapter. But as the earliest versions of Node.js did not support Promises, the use of callbacks is still prevalent in many npm modules and existing applications.

  5. To demonstrate that this code is asynchronous, we can use the setInterval() function to print a string to the screen while the program is running. The setInterval() function enables you to schedule a function to happen at a specified delay in milliseconds. Add the following line to the end of your program:
    setInterval(() => process.stdout.write("**** \n"), 1).unref();

    Observe that the string continues to be printed every millisecond, even in between when the file is being read and rewritten. This shows that the file reading and writing have been implemented in a non-blocking manner because operations are still completing while the file is being handled.

  6. To demonstrate this further, you could add a delay between the reading and writing of the file. To do this, wrap the updateFile() function in a setTimeout() function. The setTimeout() function allows you to pass it a function and a delay in milliseconds:
    setTimeout(() => updateFile(filepath, upperContents), 10);
  7. Now the output from our program should have more asterisks printed between the file read and write, as this is where we added the 10ms delay:
    $ node file-async.js
    **** 
    **** 
    File Contents: HELLO WORLD!
    **** 
    **** 
    **** 
    **** 
    **** 
    **** 
    **** 
    **** 
    **** 
    File updated

We can now see that we have converted the program from the Managing files with fs module recipe to handle the file operations asynchronously using the callback syntax.

Using the fs Promises API

The fs Promises API was released in Node.js v10.0.0. The API provides File System functions that return Promise objects rather than callbacks. Not all of the original fs module APIs have equivalent Promise-based APIs, as only a subset of the original APIs were converted to use Promise APIs. Refer to the Node.js API documentation for the full list of fs functions provided via the fs Promises API: https://nodejs.org/dist/latest/docs/api/fs.html#fs_fs_promises_api.

A Promise is an object that is used to represent the completion of an asynchronous function. The naming is based on the general definition of the term Promise—an agreement to do something or that something will happen. A Promise object is always in one of the three following states:

  • Pending
  • Fulfilled
  • Rejected

A Promise will initially be in the pending state and will remain in the pending state until it becomes either fulfilled—when the task has completed successfully—or rejected—when the task has failed:

  1. To use the API, you'll first need to import it:
    const fs = require("fs").promises;
  2. It is then possible to read the file using the readFile() function:
    fs.readFile(filepath, "utf8").then((contents) => {
        console.log("File Contents:", contents);
    });
  3. You can also combine the fs Promises API with the use of the async/await syntax:
    const fs = require("fs").promises;
    const path = require("path");
    const filepath = path.join(process.cwd(), "hello.txt");
    async function run() {
      try {
        const contents = await fs.readFile(filepath, "utf8");
        console.log("File Contents:", contents);
      } catch (error) {
        console.error(error);
      }
    }
    run();

Now we've learned how we can interact with files using the fs Promises API.

Important note

It was necessary to wrap the async/await example in a function as await must only be called from within an async function. There is an active proposal at ECMA TC39, the standardization body for ECMAScript (JavaScript), to support Top-Level Await, which would enable you to use the await syntax outside of an async function.

See also

  • The Inspecting file metadata recipe in this chapter
  • The Watching for file updates recipe in this chapter
 

Inspecting file metadata

The fs module generally provides APIs that are modeled around Portable Operating System Interface (POSIX) functions. The fs module includes APIs that facilitate the reading of directories and file metadata.

In this recipe, we will create a small program that returns information about a file, using functions provided by the fs module.

Getting ready

Get started by creating a directory to work in:

$ mkdir fetching-metadata $ cd fetching-metadata

We'll also need to create a file to read and a file for our program:

$ touch metadata.js $ touch file.txt

How to do it

Using the files created, we will create a program that gives information about the file we pass to it as a parameter:

  1. As in the previous handling I/O recipes, we first need to import the necessary core modules. For this recipe, we just need to import the fs module:
    const fs = require("fs");
  2. Next, we need the program to be able to read the filename as a command-line argument. To read the file argument, we can use process.argv[2]. Add the following line to your program:
    const file = process.argv[2];
  3. Now, we will create our printMetadata() function:
    function printMetadata(file) {
      const fileStats = fs.statSync(file);
      console.log(fileStats);
    }
  4. Add a call to the printMetadata() function:
    printMetadata(file);
  5. You can now run the program, passing it the./file.txt parameter. Run your program with the following:
    $ node metadata.js ./file.txt
  6. Expect to see output similar to the following:
    Stats {
      dev: 16777224,
      mode: 33188,
      nlink: 1,
      uid: 501,
      gid: 20,
      rdev: 0,
      blksize: 4096,
      ino: 3684101,
      size: 0,
      blocks: 0,
      atimeMs: 1585337526680.254,
      mtimeMs: 1585337526680.254,
      ctimeMs: 1585337722432.1917,
      birthtimeMs: 1585337526680.254,
      atime: 2020-03-27T19:32:06.680Z,
      mtime: 2020-03-27T19:32:06.680Z,
      ctime: 2020-03-27T19:35:22.432Z,
      birthtime: 2020-03-27T19:32:06.680Z
    }
  7. You can try adding some random text to file.txt and rerun your program; observe that the size and mtime values have been updated.
  8. Now let's see what happens when we pass a non-existent file to the program:
    $ node metadata.js ./not-a-file.txt
    internal/fs/utils.js:230
        throw err;
        ^
    Error: ENOENT: no such file or directory, stat 'nofile'

    The program throws an exception.

  9. We should catch this exception and output a message to the user saying the file path provided does not exist. To do this, change the printMetadata() function to this:
    function printMetadata(file) {
      try {
        const fileStats = fs.statSync(file);
        console.log(fileStats);
      } catch (err) {
        console.error("Error reading file path:", file);
      }
    }
  10. Run the program again with a non-existent file; this time you should see that the program handled the error rather than throwing an exception:
    $ node metadata.js ./not-a-file.txt
    Error reading file: undefined

How it works

process.argv is a property on the global process object that returns an array containing the arguments that were passed to the Node.js process. The first element of the process.argv array, process.argv[0] is the path of the node binary that is running. The second element is the path of the file we're executing, in this case, meta.js. In the recipe, we passed the filename as the third command-line argument and therefore referenced it with process.argv[2].

Next, we created a printMetadata() function that called statSync(file). statSync() is a synchronous function that returns information about the file path that is passed to it. The file path passed can be either a file or a directory. The information returned is in the form of a stats object. The following table lists the information returned on the stats object:

Figure 2.1 – Table listing properties returned on the Stats object

Figure 2.1 – Table listing properties returned on the Stats object

Important note

In this recipe, we used only the synchronous File System APIs. For most of the fs APIs, there are both synchronous and asynchronous versions of each function. Refer to the Working with files asynchronously section of the previous recipe for more information about using asynchronous File System APIs.

In the final steps of this recipe, we edited our printMetadata() function to account for invalid file paths. We did this by wrapping the statSync() function in a try/catch statement.

There's more

Next, we'll look at how we can check file access and modify file permissions and how to examine a symbolic link (symlink).

Checking file access

It is recommended that if you're attempting to read, write, or edit a file, you follow the approach of handling the error if the file is not found, as we did in the recipe.

However, if you simply wanted to check the existence of a file, you could use the fs.access() or fs.accessSync() APIs. Specifically, the fs.access() function tests the user's permissions for accessing the file or directory passed to it. The function also allows an optional argument of mode to be passed to it, where you can request the function to do a specific access check using the Node.js File Access Constants. This list of Node.js File Access Constants is available in the Node.js fs module API documentation: https://nodejs.org/api/fs.html#fs_file_access_constants. These enable you to check whether the Node.js process can read, write, or execute the file path provided.

Important note

There is a legacy API that is now deprecated called fs.exists(). It is not recommended you use this function—the fs.access() APIs should be used instead. 

Modifying file permissions

The Node.js fs module provides APIs that can be used to alter the permissions on a given file. As with many of the other fs functions, there is both an asynchronous API, chmod(), and an equivalent synchronous API, chmodSync(). Both functions take a file path and mode as the first and second arguments, respectively. The asynchronous function accepts a third parameter, which is the callback function to be executed upon completion.

Important note

chmod is a command that is used to change access permissions of file system objects on Unix and similar operating systems. If you're unfamiliar with Unix file permissions, it is recommended you refer to the Unix manual pages (https://linux.die.net/man/1/chmod).

The mode argument can be either in the form of a numeric bitmask using a series of constants provided by the fs module or a sequence of three octal digits. The constants that can be used to create the bitmask to define user permissions are defined in the Node.js API documentation: https://nodejs.org/api/fs.html#fs_file_modes.

Imagine that you have a file that currently has the following permissions:

  • Owner readable and writeable
  • Group readable
  • Readable only by all other users (sometimes referred to as world readable)

If we wanted to additionally grant write access to those in the same group in our shell, we could use the following Node.js code:

const fs = require("fs");
const file = "./file.txt";
fs.chmodSync(
  file,
  fs.constants.S_IRUSR |
    fs.constants.S_IWUSR |
    fs.constants.S_IRGRP |
    fs.constants.S_IWGRP |
    fs.constants.S_IROTH
);

As you can see, this code is quite verbose. Adding a complex series of permissions would require passing a numerous amount of constants to create the numeric bitmask. Alternatively, we can pass the chmodSync() function the octal representation of file permissions, similar to how you can when using the Unix chmod command. We're going to change the permissions using the equivalent of chmod 664 from the command line, but via Node.js:

const fs = require("fs");
const file = "./file.txt";
fs.chmodSync(file, 0o664);

Important Note

Refer to https://mason.gmu.edu/~montecin/UNIXpermiss.htm for more detailed information on how Unix permissions work.Windows File Permissions: The Windows operating system does not have as refined file permissions as on Unix—it is only possible to denote a file as writeable or non-writeable.

Inspecting symbolic links

A symbolic link, or symlink, is a special file that stores a reference to another file or directory. When the stat or statSync() function from the Inspecting file metadata recipe is run on a symbolic link, it will return information about the file the symbolic link references, rather than the symbolic link itself.

The Node.js fs module does, however, provide the functions named lstat() and lstatSync() that inspect the symbolic link itself:

  1. To create a symbolic link, you can use the following command:
    $ ln -s file.txt link-to-file
  2. Now, you can use the Node.js REPL (Read-Eval-Print Loop) to test the lstatSync() function. The Node.js REPL is an interactive shell we can pass statements to, and it will evaluate them and return the result to the user.
  3. To enter the Node.js REPL, type node in your shell:
    $ node
    Welcome to Node.js v14.0.0.
    Type ".help" for more information.
    >
  4. You can then type commands such as the following:
    > console.log("Hello World!");
    Hello World!
    Undefined
  5. Now you can try out the lstatSync command:
    > fs.lstatSync("link-to-file"); 
    Stats {
      dev: 16777224,
      mode: 41453,
      nlink: 1,
      ...
    }

Note that we did not need to explicitly import the Node.js fs module. The REPL automatically loads the core (built-in) Node.js modules so that they are available to be used. The REPL is a useful tool for testing out commands without having to create new files.

See also

  • The Handling standard I/O recipe in this chapter
 

Watching for file updates

Node.js's fs module provides functionality that enables you to watch files and track when files or directories are created, updated, or deleted.

In this recipe, we'll create a small program named watch.js that watches for changes in a file using the watchFile() API and then prints a message when a change has occurred.

Getting ready

  1. For this recipe, we'll want to work inside a new directory. Create and change into a directory called file-watching:
    $ mkdir file-watching
    $ cd file-watching
  2. We need to also create a file that we can watch:
    $ echo "Hello World!" > file.txt 	
  3. Create the watch.js file:
    $ touch watch.js

Now that we have created our directory and file, we can move on to the recipe.

How to do it

We're going to create a program that watches for changes in a file. We will be using the fs module and, specifically, the watchFile() method to achieve this:

  1. To get started, import the required core Node.js modules:
    const fs = require("fs");
  2. We also need the program to access a file we created:
    const file = "./file.txt";
  3. Next, we call the fs.watchFile() function:
    fs.watchFile(file, (current, previous) => {  
        return console.log(`${file} updated ${(current.mtime)}`);
    });
  4. Now, we can run the program in the Terminal with this: 
    $ node watch.js 
  5. In your editor, open file.txt and make some edits, saving between each one. You will notice that each time you save, a log entry appears in the Terminal where you're running watch.js:
    ./file.txt updated Wed Mar 25 2020 00:38:31 GMT+0000 (Greenwich Mean Time)
  6. While we're here, we can make the timestamp more readable. To do this, we're going to use the moment.js module. It is an external module that enables you to manipulate dates and times in JavaScript.
  7. First, we need to initialize a new project. Do this by typing $ npm init --yes. Chapter 5, Developing Node.js Modules will go into more detail about this command. For now, we'll pass the --yes option to accept the defaults. You should now have a package.json file in your project directory.
  8. Now we can install the moment.js module. Note that this step will require an internet connection, as the package will be downloaded from the public npm registry:
    $ npm install moment 

    If you open package.json, you will notice that moment has been added under the dependencies field.

  9. We now need to import moment into our watch.js file. Add the following, just below your file constant declaration:
    const moment = require("moment");
  10. Add and change the following lines to format the date using moment.js:
      const time = moment().format("MMMM Do YYYY, h:mm:ss a");
      return console.log(`${filename} updated ${time}`);
  11. Rerun the program and make further edits to file.txt—observe that the time is now in a more readable format:
    $ node watch.js
    ./file.txt updated March 27th 2020, 3:38:27 pm

How it works

In the recipe, we used the watchFile() function to watch for changes on a given file path. The function accepts three arguments—a filename, a list of options, and a listener function. The options object can include the following:

  • BigInt: This defaults to false; when set to true, the numeric values returned from the object of Stats would be specified as BigInt. BigInt is a JavaScript object that allows you to represent larger numbers more reliably.
  • Persistent: This value indicates whether the Node.js process should continue to run while files are still being watched. It defaults to true.
  • Interval: The interval value controls how often the file should be polled for changes, measured in milliseconds. The default value is 5,007 milliseconds when no interval is supplied.

The listener function supplied to the watchFile() function will execute every time a change is detected. The listener function's arguments current and previous are both Stats objects, representing the current and previous state of the file.

Our listener function passed to watchFile() is executed each time a change has been detected in the file being watched. Every time the file is updated, our listener function logs the message to STDOUT.

The Node.js fs module provides another function watch that watches for changes in files but can also watch for directories. This function differs from watchFile() as it utilizes the operating system's underlying file system notification implementation, rather than polling for changes.

Although faster and more reliable than the watchFile() API, the Watch API is not consistent across various platforms. This is because the Watch API is dependent on the underlying operating systems method of notifying file system changes. The Node.js API documentation goes into more detail about the limitations of the Watch API across different platforms: https://nodejs.org/docs/latest/api/fs.html#fs_availability.

The watchFile() function accepts three parameters—the file path, an array of options, and a listener function. The options that can be passed via the options parameter are as follows:

  • Persistent: The persistent option is a Boolean that indicates whether the Node.js process should continue to run while files are still being watched. By default, the persistent option is set to true.
  • Recursive: The recursive option is another Boolean that allows the user to specify whether changes in subdirectories should be watched – by default, this value is set to false. The recursive option is only supported on macOS and Windows operating systems.
  • Encoding: The encoding option is used to specify which character encoding should be used for the filename specified—the default is utf8.

The listener function that is passed to the watch() API is slightly different to the listener function passed to the watchFile() API. The arguments to the listener function are eventType and trigger, where eventType is either change or rename and trigger is the file that triggered an event. The following code represents a similar task to what we implemented in our recipe but using the Watch API:

const fs = require("fs");
const file = "./file.txt";
const moment = require("moment");
fs.watch(file, (eventType, filename) => {
    const time = moment().format("MMMM Do YYYY, h:mm:ss a");
    return console.log(`${filename} updated ${time}`);
});

The final steps of the recipe cover installing, importing, and using the npm module, moment.js. moment.js is a popular JavaScript library that enables users to parse, validate, and display dates and times. In the recipe, we used the module to format the last updated time in a more readable date and time format, MMMM DD YYYY, h:mm:ss a. It is possible to customize how you want moment.js to display the date format, as in this example:

moment().format('dddd'); // Saturday
moment().format("MMM Do YY"); // Mar 28th 20
moment().format(); // 2020-03-28T16:59:14+00:00

Refer to the Moment.js documentation for the list of available formats and APIs: https://momentjs.com/docs/.

See also

  • The Consuming Node.js modules recipe in Chapter 5, Developing Node.js Modules
 

Creating TCP server and client communication

Sockets allow machines and devices to communicate. Sockets are also used to coordinate I/O across networks. The term socket is used to refer to one endpoint of a two-way network communication link. Sockets enable us to build real-time web applications, such as instant messaging applications.

In this recipe, we will create a TCP server and a TCP client and allow them to communicate. TCP stands for Transmission Control Protocol. TCP provides a standard that allows devices to communicate over a network.

Getting ready

First, let's create a directory to work in:

$ mkdir communicating-with-sockets
$ cd communicating-with-sockets

We'll also need two separate files, one for the server and one for the client:

$ touch server.js
$ touch client.js

How to do it

First, we're going to create a TCP server using the net Node.js core module:

  1. We need to import the net module in server.js:
    const net = require("net");
  2. Now let's set up some variables to store the hostname and port that we want our server to run on:
    const HOSTNAME = "localhost";
    const PORT = 3000;
  3. Now, we can create the server, passing the HOSTNAME and PORT variables into the listen() function:
    net
      .createServer((socket) => {
        console.log("Client connected.");
      })
      .listen(PORT, HOSTNAME);
  4. Now we should add some socket event listeners. Add the following two event listeners below the console.log("Client connected."); line:
        socket.on("data", (name) => {
          socket.write(`Hello ${name}!`);
        });
  5. Now let's create the client. Again, we need to start by importing the net module in client.js:
    const net = require("net");
  6. Next, we can try and connect to the server we configured in server.js. We'll define the HOSTNAME and PORT variables again in this file:
    const HOSTNAME = "localhost";
    const PORT = 3000;
    const socket = net.connect(PORT, HOSTNAME);
  7. Now that we've connected to that socket, we can write to it:
    socket.write("World");
  8. We also need to add a function that will listen for data returned by the socket:
    socket.on("data", (data) => {
      console.log(data.toString());
    });
  9. Run your server with the following command:
    $ node server.js
  10. In a second shell, run client.js:
    $ node client.js
  11. In the shell where you're running server.js, you should see the output, Client connected:
    $ node server.js
    Client connected.
  12. And in the shell where you're running client.js, you should see that the socket has responded with Hello World!:
    $ node client.js
    Hello World!

We've successfully set up a TCP server and client and allowed them to communicate via sockets.

How it works

The first half of the recipe focused on creating a TCP server.

The recipe used the createServer() function from the core Node.js to http module and the net function to create the server. The function passed to createServer() accepts a function, and this function is executed each time a new connection is made to the server. The Node.js API documentation describes this function as a connectionListener function.

socket is passed as an argument to this connection listener function. It is possible to listen for events on the socket object. In the recipe, we listened for the data event and registered a function to execute each time data was received.

We then called the listen() function on createServer()—this function starts the server listening for connections. In the recipe, we passed the listen() function the hostname and port that we wanted the server to be accessible at. Note that it is also possible to listen on a Unix socket such as the following:

	const socket = net.connect("/tmp/my.socket");

Similarly, we also registered a data listener in client.js, which was listening for data written back from the server.

There are many other events that can be listened for on socket objects:

Figure 2.2 – Table listing socket events

Figure 2.2 – Table listing socket events

There's more

For some communications, UDP is more appropriate than TCP. Let's take a look at what UDP sockets are, what they're used for, and how to implement a UDP socket.

UDP stands for User Datagram Protocol and is an alternative to TCP. UDP is a connectionless protocol. Unlike TCP, the protocol does not establish a connection before sending data. UDP also doesn't guarantee delivery of packets—some can be lost. UDP is most often used in cases where speed is considered more important than reliability. UDP is typically used for video calling, gaming, or streaming—because in these cases, minimizing delay is important.

Node.js provides a core module named dgram that provides APIs to interact with UDP sockets. As with the other core modules, it can be imported with the following:

const dgram = require("dgram");

To create a socket, the dgram module exposes a createSocket() API:

const socket = dgram.createSocket("udp6");

We pass the udp6 function to instruct that we'd like the socket to interface over both IPv4 and IPv6.

To instruct the socket to start listening for connections, you use the bind function:

socket.bind(PORT);

Note that it is not necessary to provide a port. If none is provided (or you provide 0); the operating system will bind to a random free port.

About the Author

  • Bethany Griggs

    Bethany Griggs is a Software Engineer and scrum master working within the IBM Runtime Technologies Node.js team since 2016. She is an active open-source developer and a part of the Node.js Technical Steering Committee Member

    Browse publications by this author

Latest Reviews

(1 reviews total)
Can’t recommend this book enough to other Node.is developers who seriously want to build modern web apps.

Recommended For You

Node Cookbook - Fourth Edition
Unlock this book and the full library for FREE
Start free trial