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:
- 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 });
- 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
- Exit the program using CTRL + C.
- 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}!`);
- 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
- 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
- Chapter 3, Streams, streams, streams
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
- Create another directory for this recipe:
$ mkdir working-with-files $ cd working-with-files
- 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
- 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, whentouch
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:
- We'll start by requiring the built-in modules
fs
andpath
. Add the following lines toreadWriteSync.js
:const fs = require("fs"); const path = require("path");
- 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");
- We can now synchronously read the file contents using the
readFileSync()
function provided by thefs
module. We'll also print the file contents to STDOUT usingconsole.log()
:const contents = fs.readFileSync(filepath, "utf8"); console.log("File Contents:", contents);
- Now, we can edit the content of the file—we will convert the lowercase text into uppercase:
const upperContents = contents.toUpperCase();
- 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.");
- 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.
- 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.
- 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."); }); });
- 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, () => {}); }); });
- 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. - 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. ThesetInterval()
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.
- 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 asetTimeout()
function. ThesetTimeout()
function allows you to pass it a function and a delay in milliseconds:setTimeout(() => updateFile(filepath, upperContents), 10);
- 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:
- To use the API, you'll first need to import it:
const fs = require("fs").promises;
- It is then possible to read the file using the
readFile
() function:fs.readFile(filepath, "utf8").then((contents) => { console.log("File Contents:", contents); });
- You can also combine the
fs
Promises API with the use of theasync/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:
- 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");
- 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];
- Now, we will create our
printMetadata()
function:function printMetadata(file) { const fileStats = fs.statSync(file); console.log(fileStats); }
- Add a call to the
printMetadata()
function:printMetadata(file);
- You can now run the program, passing it the
./file.txt
parameter. Run your program with the following:$ node metadata.js ./file.txt
- 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 }
- You can try adding some random text to
file.txt
and rerun your program; observe that thesize
andmtime
values have been updated. - 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.
- 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); } }
- 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
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:
- To create a symbolic link, you can use the following command:
$ ln -s file.txt link-to-file
- 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. - To enter the Node.js REPL, type
node
in your shell:$ node Welcome to Node.js v14.0.0. Type ".help" for more information. >
- You can then type commands such as the following:
> console.log("Hello World!"); Hello World! Undefined
- 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
- 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
- We need to also create a file that we can watch:
$ echo "Hello World!" > file.txt
- 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:
- To get started, import the required core Node.js modules:
const fs = require("fs");
- We also need the program to access a file we created:
const file = "./file.txt";
- Next, we call the
fs.watchFile()
function:fs.watchFile(file, (current, previous) => { return console.log(`${file} updated ${(current.mtime)}`); });
- Now, we can run the program in the Terminal with this:
$ node watch.js
- 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 runningwatch.js
:./file.txt updated Wed Mar 25 2020 00:38:31 GMT+0000 (Greenwich Mean Time)
- 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. - 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 apackage.json
file in your project directory. - 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 publicnpm
registry:$ npm install moment
If you open
package.json
, you will notice thatmoment
has been added under thedependencies
field. - We now need to import
moment
into ourwatch.js
file. Add the following, just below your file constant declaration:const moment = require("moment");
- 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}`);
- 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 totrue
, the numeric values returned from the object ofStats
would be specified asBigInt
.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:
- We need to import the
net
module inserver.js
:const net = require("net");
- 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;
- Now, we can create the server, passing the
HOSTNAME
andPORT
variables into thelisten()
function:net .createServer((socket) => { console.log("Client connected."); }) .listen(PORT, HOSTNAME);
- 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}!`); });
- Now let's create the client. Again, we need to start by importing the
net
module inclient.js
:const net = require("net");
- Next, we can try and connect to the server we configured in
server.js
. We'll define theHOSTNAME
andPORT
variables again in this file:const HOSTNAME = "localhost"; const PORT = 3000; const socket = net.connect(PORT, HOSTNAME);
- Now that we've connected to that socket, we can write to it:
socket.write("World");
- We also need to add a function that will listen for data returned by the socket:
socket.on("data", (data) => { console.log(data.toString()); });
- Run your server with the following command:
$ node server.js
- In a second shell, run
client.js
:$ node client.js
- In the shell where you're running
server.js
, you should see the output,Client connected
:$ node server.js Client connected.
- And in the shell where you're running
client.js
, you should see that the socket has responded withHello 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
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.