Node JS

Node JS -- also known as Node.js -- is practically at the center of all modern JavaScript development. Node JS plays a critical role in the modern JavaScript ecosystem, because it's used to run all kinds of JavaScript logic, and not just the JavaScript UI driven logic run on browsers (e.g. clicks, scrolls, hovers), but rather more advanced JavaScript (e.g. file system access and advanced networking). In addition, Node JS is also designed to administer JavaScript packages, which in turn allow Node JS to run more complex JavaScript applications built on these JavaScript packages.

Before jumping into how Node JS works, let's take a quick look at how Node JS compares to other software you've probably worked with, namely JavaScript browser engines and other programming language run-time environments, in this manner, you'll gain a better understanding of the tasks Node JS is designed to perform.

How Node JS compares to JavaScript browser engines and other programming language run-time environments

In most programming languages like Python, PHP or Ruby, when you install their run-time environments, an installation is not only equipped to run the entire set of instructions available in a programming language, it's also equipped with a set of staple libraries/modules to execute run of the mill programming tasks (e.g. read/write files, establish network connections), as well as the ability to leverage third party libraries/modules to execute practically any kind of programming tasks. This type of architecture which is available out-of-the-box in most programming language run-times, is one of the major voids Node JS fills over JavaScript engines included in browsers and it's why Node JS has become such a dominant player in the modern JavaScript ecosystem.

If you view Node JS from the perspective of other programming language run-time environments, Node JS is like an enhanced JavaScript run-time environment, because it resembles what most programming language run-times offer out-of-the-box. Figure 8-1 illustrates the resemblance of a Node JS installation to a Python installation.

Node JS installation vs. Python installation
Figure 8-1. Node JS installation vs. Python installation

When you perform a Python installation it comes equipped with the features you see to the right side of figure 8-1. For starters, a Python installation allows the execution of the Python language, but in addition comes equipped with a set of handy Python modules to execute common programming tasks in Python (e.g. read/write files, establish network connections). In addition, a Python installation also comes equipped with a package manager -- named pip -- designed for the installation and management of third party Python packages to aid in the execution of more advanced programming tasks in Python (e.g. web frameworks, business analytics).

On the left side of figure 8-1, you can see that a Node JS installation at its core uses the same V8 JavaScript engine built-in to the Google Chrome browser. So does this mean Node JS is like a browser ? No, Node JS leverages the same V8 JavaScript engine used by a browser to process JavaScript, but this is the only thing Node JS has in common with a browser. In figure 8-1 you can also see a Node JS installation comes equipped with a set of handy built-in JavaScript modules to execute common programming tasks. In addition, a Node JS installation also comes equipped with the npm package manager designed for the installation and management of third party JavaScript packages to aid in the execution of more advanced programming tasks in JavaScript (e.g. web frameworks, business analytics).

With this overview of what constitutes a Node JS installation, you can understand why the built-in JavaScript installations of mass-market browsers with their more limited functionalities have never been an option for modern JavaScript development. It required looking beyond the features offered by built-in JavaScript installations of mass-market browsers and getting insight from what other programming languages offered out-of-the-box, for Node JS to come into existence and become a dominant force in modern JavaScript projects.

Node JS - Installation and versioning

Since Node JS is a core piece of software that runs JavaScript, like all other core pieces of software that run programming languages, it's generally very easy to install, but it can have a myriad of behavioral differences if you don't use a specific version for a given JavaScript application.

Node JS has been released in over fifteen major versions, with even numbered versions (e.g. 14, 12, 10) representing Long Term Support (LTS) releases -- which means they're maintained for a longer period of time, approximately 30 months -- whereas odd numbered versions (e.g. 15, 13, 11) have quicker End of Life (EOL) timespans -- which means as soon as a new version is released, approximately every 6 months, the prior version is no longer updated. The finer details of the Node JS version release strategy[1] can be a little complex to follow, so for practical purposes, I recommend you stick to using Node JS LTS releases or whatever Node JS version the provider of a given JavaScript application recommends.

At the time of this writing, Node JS 14 is the latest LTS release, so steps outlined from this point on are based on the use of Node JS 14. I can't emphasize enough how using Node JS 14 is no guarantee that it will work on all software that requires Node JS. In fact, from personal experience I can say that sometimes even minor Node JS version variations (e.g. 12.1.0 to 12.8.0) can break software, never mind major Node JS version variations (e.g. 10 to 12 or 12 to 14), so if a piece of JavaScript software recommends using Node JS version x.x, use that Node JS version to avoid any unexpected behaviors.

When it comes to installation, Node JS is available for many operating systems and processor architectures[2], in addition to being available in source code so it can also be built to run on any platform. In addition, some operating systems have turn-key installation support for Node JS through their package managers (e.g. apt, rpm), albeit this last approach rarely supports the most up to date Node JS version, so it's often best to directly download a specific Node JS installation package instead of relying on an operating system package manager.

In most cases, a few mouse clicks or command line instructions will be sufficient to install Node JS. But in case you get stuck during the installation process, I advise you to look over other resources on the web for a possible solution to your installation problems, as covering Node JS installation problems would go beyond the scope of how Node JS works. Being such a widely adopted platform, it's very likely someone else has encountered and documented a possible solution to a given Node JS installation problem.

Once you successfully install Node JS, it will have a bin folder with the following executables:

The Node version manager nvm (Optional)

While Node JS offers access to the aforementioned executables -- node, npm & npx -- there's the limitiation that an operating system is forced to work with a single set of these Node JS executables. In other environments this might not be a problem, but as I mentioned a few paragraphs ago, applications that rely on Node JS can be pretty finicky when it comes to what Node version they run on (e.g. an application could run on Node JS 14.5.0, but present problems with Node JS 14.6.0). Therefore, it's always convenient to have access to multiple Node JS versions -- or more specifically various sets of node, npm & npx executables.

The Node version manager[3] is a tool that allows you to run different Node JS versions on the same operating system. Although the use of the Node version manager is optional -- since it's a completely separate development from Node JS -- I highly recommend you use of it, because not only does it come with a small learning curve, it's also unobtrusive, since you can install it and forget about it until the need arises to use multiple Node JS versions.

To install the Node version manager you need to download it and run its bash script with the following command: wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash. Once installed, you'll have access to the nvm executable. Next, invoke the nvm current command as shown in listing 8-1, followed by the other commands in listing 8-1.

Listing 8-1. Node version manager nvm current version, with node, npm and npx versions.
[user@laptop]$ nvm current
v14.5.0
[user@laptop]$ node --version
v14.5.0
[user@laptop]$ npm --version
6.14.5
[user@laptop]$ npx --version
6.14.5

The output for nvm current in listing 8-1 indicates Node version manager is running Node JS version 14.5.0. Next, to confirm this information you can see in listing 8-1 the output for node --version also points to version 14.5.0, whereas npm --version and npx --version point to version 6.14.5.

Next, use the command nvm ls-remote to get a list of all the available/installable Node JS versions. Among the list you'll see version 15.14.0, which is one of the latest Node JS releases, let's proceed to install it as shown in listing 8-2.

Listing 8-2. Node version manager nvm install version.
[user@laptop]$ nvm install 15.14.0
Downloading and installing node v15.14.0...
Downloading https://nodejs.org/dist/v15.14.0/node-v15.14.0-linux-x64.tar.xz...
Computing checksum with sha256sum
Checksums matched!
Now using node v15.14.0 (npm v7.7.6)
[user@laptop]$ node --version
v15.14.0
[user@laptop]$ npm --version
7.7.6
[user@laptop]$ npx --version
7.7.6

As you can see in listing 8-2, the command nvm install <version_number> triggers the download for the specified Node JS version -- 15.14.0 in this case -- followed by its installation. You can also see that as part of this command, the default Node JS version used by Node version manager is also updated to 15.14.0. Listing 6.2 also confirms the update to the default Node JS version, with the node --version command that outputs version 15.14.0 and npm --version and npx --version which output version 7.7.6.

To move between Node JS versions and update an operating system's node, npm and npx executables, you can use the nvm list command to first confirm all locally installed Node JS versions, followed by the nvm use <version_number> to switch versions, as shown in listing 8-3.

Listing 8-3. Node version manager nvm list all versions and update default Node JS version.
[user@laptop]$ nvm list
       v12.18.1
       v14.5.0
       v15.10.0
->     v15.14.0
[user@laptop]$ nvm use 14.5.0
Now using node v14.5.0 (npm v6.14.5)
daniel@daniel-desktop:/tmp$ nvm list
       v12.18.1
->     v14.5.0
       v15.10.0
       v15.14.0

Listing 8-3 shows the nvm list outputs four locally installed versions, notice the -> arrow to the left of 15.4.0 version indicating it's the active version. Next, nvm use 14.5.0 tells Node version manager to update the default Node JS version back to version 14.5.0, notice the result of this instruction is Now using node v14.5.0 (npm v6.14.5) indicating the node executable is now running version 14.5.0 and the npm & npx executables are running version 6.14.5. Finally, listing 8-3 illustrates that running nvm list once more now highlight's version 14.5.0 as the default.

As you can see from these brief instructions, the Node version manager makes installing, updating and running multiples Node JS versions on the same operating system quite easy.

Tip The Node version manager has many more options, use nvm --help to get a full list.

The Node JS node command

The node command is the main Node JS executable and is one of three major binaries included with Node JS. If you have a background in another programming language, you can think of the node executable as the equivalent to a javascript executable, similar to Python's python executable or PHP's php executable which are at the center of each programming language's actions.

Like all other main executables in programming language run-time environments, the Node JS node command has a wealth of options and environment variables you can use to modify its default behavior, which include behaviors for debugging, security, profiling and experimental features, among other things.

If you execute node with the --help flag (e.g. node --help) you'll see the list of over fifty Node JS options and over fifteen Node JS environment variables. The majority of the time you'll only use a few of these options, which are the ones I'll describe in the upcoming sections.

A JavaScript REPL

REPLs are a common tool in many programming languages to test out language statements without the need to perform complex setups. The node REPL mode provides access to a JavaScript environment in which you can interactively evaluate JavaScript statements.

Go ahead and execute node without any arguments to enter the REPL mode as illustrated in listing 8-4.

Listing 8-4. node REPL mode
[user@laptop]$ node
Welcome to Node.js v14.5.0.
Type ".help" for more information.
> .help
.break    Sometimes you get stuck, this gets you out
.clear    Alias for .break
.editor   Enter editor mode
.exit     Exit the repl
.help     Print this help message
.load     Load JS from a file into the REPL session
.save     Save all evaluated commands in this REPL session to a file

Press ^C to abort current expression, ^D to exit the repl
>

When you execute node it displays the Node JS version, followed by a help message and presents a prompt > awaiting further instructions. Go ahead and type .help, which as shown in listing 8-4 further displays other commands available in the node REPL mode.

At this point, you can enter any JavaScript statement at the node REPL prompt to be evaluated. Listing 8-5 contains a series of JavaScript statements you can try out.

Listing 8-5. JavaScript statements evaluated in node REPL mode
> Math.PI
3.141592653589793
> 2**5
32
> var number = 1
undefined
> let letter = 'a'
undefined
> let echoer = function(message) { 
   return message;
}
undefined
> echoer(number)
1
> echoer(letter)
'a'
> process.versions
{
  node: '14.5.0',
  v8: '8.3.110.9-node.23',
  uv: '1.38.0',
  zlib: '1.2.11',
  brotli: '1.0.7',
  ares: '1.16.0',
  modules: '83',
  nghttp2: '1.41.0',
  napi: '6',
  llhttp: '2.0.4',
  openssl: '1.1.1g',
  cldr: '37.0',
  icu: '67.1',
  tz: '2020a',
  unicode: '13.0'
}
>

The first JavaScript statement Math.PI in listing 8-5 evaluates to 3.141592653589793, what's interesting about this statement is not so much the result, but rather that the Node JS REPL provides access to the JavaScript built-in Math data type. The second JavaScript statement 2**5 ("2 to the power 5") in listing 8-5 evaluates to 32, here again the mathematical result isn't what's interesting, but rather the use of the JavaScript exponentiation operator ** which is an ES7 (ES2016) addition, confirming the Node JS REPL supports newer ECMAScript syntax.

The third JavaScript statement var number = 1 is a globally scoped variable, whereas let letter = 'a' is a block scoped variable, both variable declarations output undefined because evaluating an assignment never returns a result, however, both statements do make the number and letter references available for later access. Additional behaviors about using var and let in Node JS will be provided shortly, but for more background on why JavaScript has both var and let references see the let and const keywords: Block scoping solved, the modern years.

The fifth JavaScript statement let echoer is a function expression that returns whatever value is passed by a caller, which also outputs undefined because evaluating function expression never returns a result. The sixth and seventh JavaScript statements in listing 8-5 evaluate calling the echoer function expression with the number and letter references, evaluations that result in 1 and 'a', which are the values for number and letter, respectively.

Finally, the last JavaScript statement in listing 8-5 evaluates the process.versions reference. Although the process.versions reference is a somewhat advanced Node JS topic, it provides some interesting insight about the Node JS version itself and other dependency versions (e.g. the v8 JavaScript engine, the zlib version for compression, the openssl version for security). For example, recall back in figure 8-1 I mentioned how a Node JS installation at its core uses the same V8 JavaScript engine built-in to the Google Chrome browser, it turns out the output of the process.versions reference indicates the V8 engine used by the Node JS installation, which in this case corresponds to V8 8.3.110.9-node.23 and corresponds to the same V8 engine used by Google Chrome Browser version 83, in accordance with the V8 version numbering scheme [4].

Use double tab in the Node JS REPL for autocomplete help

When you're in Node's REPL, you can press the tab key twice to get autocomplete help on anything you type. For example, if you type Math. and then press the tab key twice, you'll get a full list of Math data type properties and methods, like the following:

> Math.
Math.__defineGetter__      Math.__defineSetter__      Math.__lookupGetter__      Math.__lookupSetter__
Math.__proto__             Math.constructor           Math.hasOwnProperty        Math.isPrototypeOf
Math.propertyIsEnumerable  Math.toLocaleString        Math.toString              Math.valueOf

Math.E                     Math.LN10                  Math.LN2                   Math.LOG10E
Math.LOG2E                 Math.PI                    Math.SQRT1_2               Math.SQRT2
Math.abs                   Math.acos                  Math.acosh                 Math.asin
Math.asinh                 Math.atan                  Math.atan2                 Math.atanh
Math.cbrt                  Math.ceil                  Math.clz32                 Math.cos
Math.cosh                  Math.exp                   Math.expm1                 Math.floor
Math.fround                Math.hypot                 Math.imul                  Math.log
Math.log10                 Math.log1p                 Math.log2                  Math.max
Math.min                   Math.pow                   Math.random                Math.round
Math.sign                  Math.sin                   Math.sinh                  Math.sqrt
Math.tan                   Math.tanh                  Math.trunc

This same autocomplete behavior is available for anything that's loaded as part of the Node JS REPL environment. For example, if you declare multiple custom variables or functions, these also become available as part of the autocomplete functionality, inclusively, if you just press the tab key twice -- without typing anything -- you'll get a list of all the available JavaScript constructs in the Node JS REPL enviornment.

Now let's use some of the Node JS REPL commands illustrated in listing 8-4. After you type JavaScript statements like the ones in 8-5 you can save them to a file for posterity with the .save command illustrated in listing 8-6.

Listing 8-6. Save statements introduced in node REPL to file
> .save myscript.js
Session saved to: myscript.js
> <Type .exit or Ctrl-D with keyboard to exit>
<Analyze contents of myscript.js>

The .save myscript.js syntax in listing 8-4 tells the Node JS REPL to save all the evaluated JavaScript statements to a file named myscript.js, where myscript.js is a file in the present working directory where the Node JS REPL was started. If you exit the Node JS REPL with the .exit command or Ctrl-D keyboard combo, you'll be able to confirm the generated file contains all the JavaScript statements introduced in the REPL session.

Now let's use the Node JS REPL .load statement illustrated in listing 8-7 to demonstrate how it's possible to renew a Node JS REPL with JavaScript statements provided in a file.

Listing 8-7. Load statements in node REPL from a file
[user@laptop]$ node
Welcome to Node.js v14.5.0.
Type ".help" for more information.
> .load myscript.js
...
...
> echoer("Hello Node JS REPL!")
'Hello Node JS REPL!'

The .load myscript.js syntax in listing 8-7 tells the Node JS REPL to load the JavaScript statements from the file named myscript.js, in this case myscript.js is the file generated in listing 8-4, but it could equally be any file with valid JavaScript statements. Once the JavaScript statements are loaded into the Node JS REPL with .load, it's possible to leverage the declarations as if you'd typed them in yourself. Notice in listing 8-7 the statement echoer("Hello Node JS REPL!") outputs 'Hello Node JS REPL!' which works because the myscript.js file has a JavaScript function expression named echoer.

A JavaScript syntax checker

Although the JavaScript REPL from the last section is one of the main offerings of the node executable, this doesn't mean it's the only practical functionality it has to offer. The node executable also supports the -c or --check flags to check JavaScript syntax. To test this node feature, I recommend you purposely modify a JavaScript file to include an invalid JavaScript statement (e.g. modify a let statment to et) and run it using the process shown in listing 8-8.

Listing 8-8. Check JavaScript syntax with node -c or node --check
[user@laptop]$ node -c myscript.js
[user@laptop]$ node -c broken_script.js
/broken_script.js:5
et echoer = function(message) { 
   ^^^^^^

SyntaxError: Unexpected identifier
    at wrapSafe (internal/modules/cjs/loader.js:1071:16)
    at checkSyntax (internal/main/check_syntax.js:69:3)
    at internal/main/check_syntax.js:39:3

The first statment node -c myscript.js in listing 8-8 outputs nothing because the myscript.js file contains valid JavaScript statements and also because the -c flag (or --check flag) simply checks for JavaScript syntax errors without executing anything. The second statment node -c broken_script.js is run against the broken_script.js file, which you can see in the output contains a SyntaxError: Unexpected identifier in line 4 of the file (i.e. broken_script.js:4 et echoer = function(message) { ^^^^^^). As you can see from the examples presented in listing 8-8, the node executable with the -c or --check flags can be helpful to quickly pinpoint JavaScript syntax errors in files of any size.

A JavaScript evaluator

In addition to Node's interactive REPL environment, Node can also directly evaluate and print JavaScript statements -- which are the Evaluate and Print in REPL. The node executable supports the -e or --eval flags to evaluate JavaScript statements, as well as the -p or --print flags to evaluate and print JavaScript statements. Listing 8-9 illustrates how to evaluate JavaScript statement with the -e or --eval flags.

Listing 8-9. Evaluate JavaScript statement with node -e or node --eval
[user@laptop]$ node -e "Math.PI"
[user@laptop]$ node -e "console.log(Math.PI)"
3.141592653589793
[user@laptop]$ node -e "let letter='a'"
[user@laptop]$ node -e "let letter='a';console.log(letter);"
a

The first statement in listing 8-9 shows evaluating the Math.PI property results in no output, because evaluating a property never returns a result, however, the next statement "console.log(Math.PI)" does print 3.141592653589793 since evaluating console.log outputs its enclosed contents, which in this case is the Math.PI property value. The third statement in listing 8-9 illustrates that evaluating an assignment doesn't return a result, whereas the fourth statement once again makes use of console.log to print the reference of the assignment statement.

Listing 8-10 illustrates how to evaluate and print JavaScript statement with the -p or --print flags.

Listing 8-10. Evaluate and print JavaScript statement with node -p or node --print flags
[user@laptop]$ node -p "Math.PI"
3.141592653589793
[user@laptop]$ node -p "let letter='a';letter;"
a

The examples in listing 8-10 are similar to those in listing 8-9, but notice the ones in listing 8-10 don't use console.log and still print a result. The reason for this behavior is because the -p and --print flags both evaluate and print statements. Therefore, the result of evaluating and printing the Math.PI property is 3.141592653589793 and the result of evaluating and printing the let letter='a';letter; statement is a, none of which require the use of console.log statements, since the -p and --print flags automatically print their output.

Node JS - A JavaScript CommonJS based sytem

The previous exercises using the node command might give you the impression the Node JS JavaScript run-time environment functions just like the one in mass-market browsers, specifically Google Chrome's V8 engine which is the one used by Node JS. This impression would be partially correct, because although standard JavaScript statements do work the same in both because they use the V8 engine, the Node JS JavaScript run-time environment does in fact work differently due to its use of JavaScript CommonJS.

As early as the modern JavaScript essentials section, I mentioned how modules, namespaces & module types were among the most important and also among the most fragmented techniques in modern JavaScript.

Node JS uses the oldest of the JavaScript module standards, CommonJS, for reasons that have more to do with "what was available at the time" than anything else. If you've never worked with JavaScript modules, to gain a better understanding of JavaScript modules in general, I recommend you review the link in the previous paragraph on modules, as well as:

What CommonJS brings to Node JS is the ability to use namespaces and avoid name clashes when running JavaScript statements from different modules, which for practical purposes modules generally equals .js files. It might not have been obvious in the previous node REPL exercises, but in Node JS, every JavaScript statement belongs to a namespace to protect it from conflicting with references in other JavaScript modules.

The Node JS global, globalThis and module objects

In the JavaScript data types chapter, I mentioned how JavaScript engines rely on a global object to keep track of references when they don't have an explicit scope. Recapping, I described how browsers use the window keyword as their global object to store a browser's built-in global references (e.g. eval(), alert()) and how Node JS uses an equivalent named global object for the same purpose, as well as how the this keyword works as an alias to access this same global object (window or global depending on the environment) and how a more recent ECMAScript standard incorporated the globalThis reference to refer to this same global object across environments (i.e. browsers and Node JS).

Let's begin the exploration of the Node JS global object and its equivalent globalThis reference with the example in listing 8-7 which loads a series of JavaScript statements provided in a file named myscript.js. Listing 8-11 begins with the same steps as listing 8-7 and then outputs the Node JS global reference, globalThis reference and this reference.

Listing 8-11. Node JS global and globalThis global object references, this also reflects the contents of the global object references.
[user@laptop]$ node
Welcome to Node.js v14.5.0.
Type ".help" for more information.
> .load myscript.js
> global
 Object [global] {
  global: [Circular *1],
  clearInterval: [Function: clearInterval],
  clearTimeout: [Function: clearTimeout],
  setInterval: [Function: setInterval],
  setTimeout: [Function: setTimeout] {
    [Symbol(nodejs.util.promisify.custom)]: [Function (anonymous)]
  },
  queueMicrotask: [Function: queueMicrotask],
  clearImmediate: [Function: clearImmediate],
  setImmediate: [Function: setImmediate] {
    [Symbol(nodejs.util.promisify.custom)]: [Function (anonymous)]
  },
  number: 1
}
> globalThis
 Object [global] {
  global: [Circular *1],
  clearInterval: [Function: clearInterval],
  clearTimeout: [Function: clearTimeout],
  setInterval: [Function: setInterval],
  setTimeout: [Function: setTimeout] {
    [Symbol(nodejs.util.promisify.custom)]: [Function (anonymous)]
  },
  queueMicrotask: [Function: queueMicrotask],
  clearImmediate: [Function: clearImmediate],
  setImmediate: [Function: setImmediate] {
    [Symbol(nodejs.util.promisify.custom)]: [Function (anonymous)]
  },
  number: 1
}
> this
<ref *1> Object [global] {
  global: [Circular *1],
  clearInterval: [Function: clearInterval],
  clearTimeout: [Function: clearTimeout],
  setInterval: [Function: setInterval],
  setTimeout: [Function: setTimeout] {
    [Symbol(nodejs.util.promisify.custom)]: [Function (anonymous)]
  },
  queueMicrotask: [Function: queueMicrotask],
  clearImmediate: [Function: clearImmediate],
  setImmediate: [Function: setImmediate] {
    [Symbol(nodejs.util.promisify.custom)]: [Function (anonymous)]
  },
  number: 1
}
> global == globalThis
true
> globalThis == this
true

After loading the statements in myscript.js -- created in listing listing 8-6 and taken from listing 8-5 -- the output for the global object only includes the number: 1 statement from the script. The reason the number reference is the only one available in the global object is because it's the only globally scoped variable, that is, var number = 1. In other words, var statements get added automatically to the JavaScript global object.

In Node JS, the global object is accesible via the global reference -- on browsers it's done through the window reference. In addition, the ES11 (ES2020) standard added the globalThis reference to access the same global object. Inclusively in these circumstances, the this reference can also be used to reference the JavaScript global object. Toward the end of listing 8-11, you can see the global reference is identical to the globalThis reference and the globalThis reference is identical to the this reference, confirming all three references point toward the global object.

Tip You should use the globalThis reference to access the global object when available (i.e. in JavaScript environments that support this ECMAScript 11 (ES2020) feature).

Although the global and this references also provide access to the global object. The global reference is Node JS specific and the this reference is an overly used reference for many other purposes in JavaScript that can get mixed up with other meanings. Therefore, the ECMAScript globalThis reference should be the preferred choice to access JavaScript's global object.

Now in this same Node JS REPL session, access the module reference, as shown in listing 8-12.

Listing 8-12. Node JS module reference
> module
Module {
  id: '<repl>',
  path: '.',
  exports: { },
  parent: undefined,
  filename: null,
  loaded: false,
  children: [],
  paths: [
    '/home/desktop/Downloads/Node14/node-v14.5.0/repl/node_modules',
    '/home/desktop/Downloads/Node14/node-v14.5.0/node_modules',
    '/home/desktop/Downloads/Node14/node_modules',
    '/home/desktop/Downloads/node_modules',
    '/home/desktop/node_modules',
    '/home/node_modules',
    '/node_modules',
    '/home/desktop/.node_modules',
    '/home/desktop/.node_libraries',
    '/home/desktop/Downloads/Node14/node-v14.5.0/out/lib/node'
  ]
}

The module reference outputs a series of characteristics associated with the current Node JS CommonJS module, which in the case of listing 8-12, is the rather obviously named repl module -- note the id: '<repl>' output.

One important characteristics of all CommonJS modules is their exports reference, which indicates what module constructs are accessible to other modules. In listing 8-12, you can see the exports value is empty {}. For the sake of completeness, let's add a couple of constructs to this CommonJS module using the syntax illustrated in listing 8-13.

Listing 8-13. Export constructs in CommonJS module with exports
> module.exports.consonant = 'b'
'b'
> module.exports.vowels = ['a','e','i','o','u']
[ 'a', 'e', 'i', 'o', 'u' ]
> module
Module {
  id: '',
  path: '.',
  exports: { consonant: 'b', vowels: [ 'a', 'e', 'i', 'o', 'u' ] },
  parent: undefined,
  filename: null,
  loaded: false,
  children: [],
  paths: [
    '/home/desktop/Downloads/Node14/node-v14.5.0/repl/node_modules',
    '/home/desktop/Downloads/Node14/node-v14.5.0/node_modules',
    '/home/desktop/Downloads/Node14/node_modules',
    '/home/desktop/Downloads/node_modules',
    '/home/desktop/node_modules',
    '/home/node_modules',
    '/node_modules',
    '/home/desktop/.node_modules',
    '/home/desktop/.node_libraries',
    '/home/desktop/Downloads/Node14/node-v14.5.0/out/lib/node'  
  ]
}

The module.exports.consonant = 'b' syntax exposes the constant reference with a value of 'b', whereas the module.exports.vowels = ['a','e','i','o','u'] syntax exposes the vowels reference with a value of ['a','e','i','o','u']. After executing the previous statements, notice the module has an updated exports value of { consonant: 'b', vowels: [ 'a', 'e', 'i', 'o', 'u' ] }. The purpose of this CommonJS technique is to be able to control what module constructs are visible to the outside world, just like it's done in other programming language module techniques. I'll elaborate further on the visibility of CommonJS modules in the next section.

Finally, you may be asking yourself where are the other myscript.js references number & echoer stored ? You can potentially access number or echoer, but because they aren't part of the global object due to their block scoped declaration (i.e.let) they aren't accessible. And while both number or echoer belong to the repl module by being created in its context, they aren't visible in the module reference either, because they aren't explicitly exported like it's done in the exercise in listing 8-13. The next section explores how to export these statements in myscript.js and make them accessible in the Node JS REPL.

Using CommonJS modules in Node JS with require

The .load Node JS mechanism that's been used up to this point is not an actual CommonJS module mechanism, it's more of a tool for copying/pasting .js file contents into the Node JS REPL. So when you do .load myscript.js, Node JS simply takes the contents of myscript.js and runs them in the Node JS REPL session.

To actually load .js files as modules in Node JS you need to use the CommonJS syntax require. Let's attempt to load the same myscript.js file into Node JS, but this time as a CommonJS module, as illustrated in listing 8-14.

Listing 8-14. Load .js file as CommonJS module with require
  
[user@laptop]$ node
Welcome to Node.js v14.5.0.
Type ".help" for more information.
> const myscript = require('./myscript.js')
undefined
> globalThis
 Object [global] {
  global: [Circular *1],
  clearInterval: [Function: clearInterval],
  clearTimeout: [Function: clearTimeout],
  setInterval: [Function: setInterval],
  setTimeout: [Function: setTimeout] {
    [Symbol(nodejs.util.promisify.custom)]: [Function (anonymous)]
  },
  queueMicrotask: [Function: queueMicrotask],
  clearImmediate: [Function: clearImmediate],
  setImmediate: [Function: setImmediate] {
    [Symbol(nodejs.util.promisify.custom)]: [Function (anonymous)]
  }
}
> myscript.letter
undefined

Once inside the Node JS REPL and ensuring the myscript.js file is in the same directory, type require('./myscript.js'). Next, notice that when accessing the globalThis object, there's no number reference anymore, even though it's declared in myscript.js. Next, try to access the letter reference which is also declared in myscript.js, it's also undefined.

So what's happening in listing 8-14 ? Why can't you access the contents of myscript.js like it's done in previous examples ? Because myscript.js was loaded as a CommonJS module, the Node JS repl module is completly isolated from the contents of this other CommonJS module.

Next, let's modify the myscript.js file so its contents are accesible to other CommonJS modules, as shown in listing 8-15.

Listing 8-15. Export constructs in .js file with CommonJS exports
Math.PI
2**5
var number = 1
let letter = 'a'
let echoer = function(message) {
  return message;
}
echoer(number)
echoer(letter)
process.versions

exports.number = number;
exports.letter = letter;
exports.echoer = echoer;

The contents in listing 8-15 are the same statements used in previous versions of myscript.js, however, notice the last three statements that begin with exports. The exports.number is CommonJS syntax that indicates to expose the number value with the number reference to other modules, whereas the exports.letter and exports.echoer perform a similar service for the number and echoer values. If you wanted to expose a value with another reference value, you can simply change the exports.<reference_to_access> = <value_to_expose> (e.g. exports.digit = number; would expose the number value with the digit reference).

With the modifications in listing 8-15 to myscript.js, let's attempt the same process from listing 8-14, as shown in listing 8-16.

Listing 8-16. Load .js file as CommonJS module with require
  
[user@laptop]$ node
Welcome to Node.js v14.5.0.
Type ".help" for more information.
> const myscript = require('./myscript.js')
undefined
> globalThis
 Object [global] {
  global: [Circular *1],
  clearInterval: [Function: clearInterval],
  clearTimeout: [Function: clearTimeout],
  setInterval: [Function: setInterval],
  setTimeout: [Function: setTimeout] {
    [Symbol(nodejs.util.promisify.custom)]: [Function (anonymous)]
  },
  queueMicrotask: [Function: queueMicrotask],
  clearImmediate: [Function: clearImmediate],
  setImmediate: [Function: setImmediate] {
    [Symbol(nodejs.util.promisify.custom)]: [Function (anonymous)]
  }
}
> myscript.number
1
> myscript.letter
'a'
> let consonant = 'b'
undefined
> myscript.echoer(consonant)
'b'
> let echoer = function(message) { 
   return "CommonJS in REPL " + message;
}
> echoer(consonant)
'CommonJS in REPL b'
> let number = 2
> number
2
> myscript.number
1

The loading of myscript.js in listing 8-16 is done just as it's in listing 8-14. First, notice the globalThis reference is still empty, as it should be, since the CommonJS module system allows the JavaScript global object to remain unpolluted with globally scoped variables. In addition, references declared in myscript.js are also available via the const myscript reference due to the various CommonJS exports statements in myscript.js. Notice that attempting to access myscript.number outputs 1 and myscript.letter outputs 'a', just like they're defined in myscript.js.

Next in listing 8-16, a block scoped consonant variable is created to invoke the myscript.echoer function expression from myscript.js. More importantly, notice how in listing 8-16 it's also possible to create another function expression called echoer that doesn't interfere with the echoer function expression in myscript.js. Similarly, notice how it's also possible to create another reference called number that's isolated from the number reference that's declared in myscript.js.

These non-clashing behaviors that deliver JavaScript namespaces, illustrate the purpose and power of CommonJS and modules in the context of Node JS.

Now that you have a basic understanding of how Node JS uses CommonJS to work with modules, let's work with some of Node JS's built-in modules.

Node JS - Built-in JavaScript modules

Node JS has over twenty built-in JavaScript modules[5] to aid in the execution of various JavaScript programming tasks -- from managing processes to network operations -- similar to those included in core language installations for Python and Java. Prior to the advent of Node JS, such tasks either required a lot of work or couldn't be done in JavaScript.

If Node JS's built-in modules aren't sufficient for your needs you can always create your own, or in all likelihood, look for a third party Node JS package to suit your needs. However, third party Node JS packages are a completely different topic and discussed in the npm chapter, which is the Node JS package manager.

Up next, we'll explore the Node JS dns module designed to perform DNS operations, as well as the Node JS fs module designed to work with files. Toward the end of this chapter, we'll also explore the Node JS http module to understand the asynchronous/callback approach & event loop design used by Node JS.

The Node JS dns module: DNS (Domain Name System) operations with JavaScript

The Node JS dns module is designed to perform DNS (Domain Name System) operations. This includes resolving domain names to IP addresses, performing reverse lookups to resolve IP addresses to host names, as well as consulting DNS records (e.g. CNAME, NS, MX, etc) associated with domains, among other DNS related operations.

Listing 8-17 illustrates how to use the Node JS dns module to resolve a domain name to an IP address.

Listing 8-17. Node JS dns module
  
[user@laptop]$ node
Welcome to Node.js v14.5.0.
Type ".help" for more information.
> const dns = require('dns')
undefined
> dns. (Press double tab)
dns.__defineGetter__      dns.__defineSetter__      dns.__lookupGetter__      dns.__lookupSetter__      dns.__proto__
dns.constructor           dns.hasOwnProperty        dns.isPrototypeOf         dns.propertyIsEnumerable  dns.toLocaleString
dns.toString              dns.valueOf

dns.ADDRCONFIG            dns.ADDRGETNETWORKPARAMS  dns.ALL                   dns.BADFAMILY             dns.BADFLAGS
dns.BADHINTS              dns.BADNAME               dns.BADQUERY              dns.BADRESP               dns.BADSTR
dns.CANCELLED             dns.CONNREFUSED           dns.DESTRUCTION           dns.EOF                   dns.FILE
dns.FORMERR               dns.LOADIPHLPAPI          dns.NODATA                dns.NOMEM                 dns.NONAME
dns.NOTFOUND              dns.NOTIMP                dns.NOTINITIALIZED        dns.REFUSED               dns.Resolver
dns.SERVFAIL              dns.TIMEOUT               dns.V4MAPPED              dns.getServers            dns.lookup
dns.lookupService         dns.promises              dns.resolve               dns.resolve4              dns.resolve6
dns.resolveAny            dns.resolveCname          dns.resolveMx             dns.resolveNaptr          dns.resolveNs
dns.resolvePtr            dns.resolveSoa            dns.resolveSrv            dns.resolveTxt            dns.reverse
dns.setServers

> dns.resolve4('modernjs.com', (error, address) => console.log('address: %j; error: %j', address, error))
QueryReqWrap {
  bindingName: 'queryA',
  callback: [Function (anonymous)],
  hostname: 'modernjs.com',
  oncomplete: [Function: onresolve],
  ttl: false
}
> address: ["96.126.116.89"]; error: null

dns.resolve6('modernjs.com', (error, address) => console.log('address: %j; error: %j', address, error))
QueryReqWrap {
  bindingName: 'queryAaaa',
  callback: [Function (anonymous)],
  hostname: 'modernjs.com',
  oncomplete: [Function: onresolve],
  ttl: false
}
> address: undefined; error: {"code":"ENODATA","syscall":"queryAaaa","hostname":"modernjs.com"}

Listing 8-17 begins like all earlier REPL exercises and then uses require('dns') to gain access to the dns module through the dns reference. Next, you can see all the available dns module properties and methods by typing the dns. reference and then pressing the tab key twice.

Listing 8-17 then illustrates a call to the resolve4 method, which resolves a domain name to its IPv4 address. The first argument to resolve4 is a domain name, while the second is a callback method, which in itself also has two arguments, to process either a failure or a successful resolution. In this case, whether a call fails or succeeds, both outcomes are output with the console.log statement inside the callback.

You can see executing the resolve4 method with the modernjs.com domain outputs address: ["96.126.116.89"]; error: null, indicating the IPv4 address for moderns.js is 96.126.116.89. Next, a similar call is made with the resolve6 method, that resolves a domain name to its IPv6 address. In this last case, you can see the output is address: undefined; error: {"code":"ENODATA","syscall":"queryAaaa","hostname":"modernjs.com"} indicating it was not possible to obtain an IPv6 address for modernjs.com with the detailed error object output.

The fs Node JS module: Read and write files with JavaScript

The Node JS fs module is designed to perform file system operations. This includes reading and writing files, performing file system operations (e.g. create directories, copy files, etc), among other file system related operations.

Now, we'll use the Node JS fs module to read a file. First, create an HTML file named index.html and put some content in it (e.g.<h1>This is an HTML page for Node JS!</h1>), place it in the same working directory where you'll start the Node JS REPL.

Listing 8-18 illustrates how to read a file with the fs module from the Node JS REPL.

Listing 8-18. Node JS fs module
  
[user@laptop]$ node
Welcome to Node.js v14.5.0.
Type ".help" for more information.
> const fs = require('fs')
undefined
> fs. (Press double tab)
fs.__defineGetter__    fs.__defineSetter__      fs.__lookupGetter__   fs.__lookupSetter__   fs.__proto__           fs.constructor         fs.hasOwnProperty
fs.isPrototypeOf       fs.propertyIsEnumerable  fs.toLocaleString     fs.toString           fs.valueOf

fs.Dir                 fs.Dirent              fs.F_OK                fs.FileReadStream      fs.FileWriteStream     fs.R_OK                fs.ReadStream
fs.Stats               fs.W_OK                fs.WriteStream         fs.X_OK                fs._toUnixTimestamp    fs.access              fs.accessSync
fs.appendFile          fs.appendFileSync      fs.chmod               fs.chmodSync           fs.chown               fs.chownSync           fs.close
fs.closeSync           fs.constants           fs.copyFile            fs.copyFileSync        fs.createReadStream    fs.createWriteStream   fs.exists
fs.existsSync          fs.fchmod              fs.fchmodSync          fs.fchown              fs.fchownSync          fs.fdatasync           fs.fdatasyncSync
fs.fstat               fs.fstatSync           fs.fsync               fs.fsyncSync           fs.ftruncate           fs.ftruncateSync       fs.futimes
fs.futimesSync         fs.lchmod              fs.lchmodSync          fs.lchown              fs.lchownSync          fs.link                fs.linkSync
fs.lstat               fs.lstatSync           fs.lutimes             fs.lutimesSync         fs.mkdir               fs.mkdirSync           fs.mkdtemp
fs.mkdtempSync         fs.open                fs.openSync            fs.opendir             fs.opendirSync         fs.promises            fs.read
fs.readFile            fs.readFileSync        fs.readSync            fs.readdir             fs.readdirSync         fs.readlink            fs.readlinkSync
fs.readv               fs.readvSync           fs.realpath            fs.realpathSync        fs.rename              fs.renameSync          fs.rmdir
fs.rmdirSync           fs.stat                fs.statSync            fs.symlink             fs.symlinkSync         fs.truncate            fs.truncateSync
fs.unlink              fs.unlinkSync          fs.unwatchFile         fs.utimes              fs.utimesSync          fs.watch               fs.watchFile
fs.write               fs.writeFile           fs.writeFileSync       fs.writeSync           fs.writev              fs.writevSync

> fs.readFile('index.html', 'utf8', (error, file_data) => console.log('file_data: %j; error: %j', file_data, error))
> file_data: "<h1>This is an HTML page for Node JS!</h1>\n"; error: null

> fs.readFile('other.html', 'utf8', (error, file_data) => console.log('file_data: %j; error: %j', file_data, error))
undefined
> file_data: undefined; error: {"errno":-2,"code":"ENOENT","syscall":"open","path":"other.html"}

Listing 8-18 uses require('fs') to gain access to the fs module through the fs reference. Next, you can see all available fs module properties and methods by typing the fs. reference and then pressing the tab key twice.

Listing 8-18 then illustrates a call to the readFile method, which reads a file from the file system. The first argument to readFile is a file name to read, the second argument is optional and indicates a file encoding -- in this case utf8 -- while the third argument is a callback method, which in itself also has two arguments, like the earlier example in listing 8-17. In this case, whether a call fails or succeeds, both outcomes are output with the console.log statement inside the callback.

You can see executing the readFile method reads the index.html file in the present directory and outputs file_data: "<h1>This is an HTML page for Node JS!</h1>\n"; error: null. Next, a similar call is made to an nonexistent other.html file, which you can see outputs file_data: undefined; error: {"errno":-2,"code":"ENOENT","syscall":"open","path":"other.html"} indicating it was not possible to read the other.html file from the present directory.

A critical aspect of both the dns and fs module examples you just explored, is their actions are asynchronous and rely on callback functions. The next section takes a closer look as this asynchronous/callback approach used in Node JS modules.

Node JS & the JavaScript asynchronous/callback approach & event loop design

If you jumped into this chapter with little to no knowledge of JavaScript or without reading earlier modernjs.com sections, it's important you understand what sets Node JS & JavaScript apart from other technology stacks, which is primarily: its asynchronous/callback approach with an event loop design.

The asynchronous/callback approach is an inherent part of JavaScript that's described in greater technical depth in JavaScript asynchronous and parallel execution chapter. But in the previous Node JS examples -- listing 8-17 & listing 8-18 -- recall that all tasks relied on callback methods. These callback methods allow the results of a given task to be acted upon until the task is finished, without the need to block subsequent tasks.

Have you ever noticed how browsers can present an alert that says "A script on this page is causing your web browser to run slowly" ? The root cause of this behavior is precisely the lack of asynchronous/callback techniques, with one task overtaking browser resources and not letting other tasks advance. Although this problem can be solved using other techniques besides asynchronous/callbacks (e.g. Web workers, browser tabs each running their own process), these techniques are more elaborate to implement and there's always the potential for issues, since JavaScript engines are inherently single threaded.

So by relying on callback methods, JavaScript is able to trigger the execution of multiple tasks without one task waiting for the results of another (e.g. Task A and Task B can begin one after the other, while they perform their work and rely on their callback methods to act once their work has finished). In this sense, callback methods work as a preemptive measure that forces you to structure program logic so that tasks don't block one another. Of course, if Task B depends on the results of Task A, it will require Task B to wait for Task A to finish, but that's another matter altogether discussed in JavaScript asynchronous behavior, the important takeaway right now is Node JS modules & packages rely a lot on the use of JavaScript callbacks.

The event loop design in JavaScript is closely related to the just described asynchronous/callback approach. In very simple terms you can think of an event loop, as a loop that's in perpetual motion that's given tasks/events to execute. So in JavaScript, Task A can be handed to the event loop, immediately followed by Task B, followed by n more tasks, all the while relying on callback methods to manage the results of each task.

Now, as great as this event loop sounds -- who wouldn't want to run tasks as quickly as possible ? -- there's a catch, all tasks/events fed to an event loop must be asynchronous in nature, otherwise you run the risk of introducing a synchronous task/event that blocks the loop! If you introduce a task/event that takes 15 minutes to return a result and it's synchronous, the event loop can hold up all other tasks/events for 15 minutes while it's done! In a browser, you'd face the issue mentioned in the previous paragraph (e.g. "A script on this page is causing your web browser to run slowly"), a Node JS application would similarly freeze or slow to a crawl. There's inclusively a dedicated Node JS document, that describes this scenario: Don't Block the Event Loop.

With an understanding of JavaScript's asynchronous/callback approach & event loop design and how it relates to Node JS, let's explore how to read a file and serve a web page in Node JS to better illustrate both these concepts.

Read a file in Node JS, revisited

In listing-8-18 you learned how to read a file in Node JS with the fs module. Next, let's create a script that reproduces this same logic, as illustrated in listing 8-19.

Listing 8-19. JavaScript-Node JS script to read file
  
const fs = require('fs');

console.log('Script start');

fs.readFile('index.html', 'utf8', (error, file_data) => console.log('file_data: %j; error: %j', file_data, error));

console.log('Script end');

Place the contents of listing 8-19 in a file named read_file_script.js and place it alongside the index.html from listing 8-18, since the script assumes it will be able to read the index.html file. Next, run the script with Node JS, as shown in listing 8-20.

Listing 8-20. JavaScript script to read file executed with Node JS
  
[user@laptop]$ node read_file_script.js
Script start
Script end
file_data: "<h1>This is an HTML page for Node JS!</h1>\n"; error: null

The most important aspect of listing 8-20 is the order of the console.log messages. Notice the second log message is Script end -- which is at the end of the script -- whereas the final log message outputs the contents of the index.html file -- which is in the middle of the script. The reason for this output order is due to the callback that reads the file. When JavaScript reaches the fs.readFile() statement, it doesn't wait for a result, instead it triggers the execution and lets the callback handle the result, immediately moving to the next statement which is console.log('Script end'). Hence Script end is output before the actual file is completely read, with the file reading completing after the end of the script is reached.

Read a file in Node JS, synchronously (Yes! It's possible)

Because the asynchronous JavaScript behavior illustrated in listing 8-20 can be hard to get used to, given it's not the standard behavior in most programming languages, the same Node JS fs module supports equivalent synchronous methods for doing file operations. If you look back at listing 8-18 you'll notice that for every method in the fs module there's an equivalent *Sync method (e.g. fs.readFile has fs.readFileSync; fs.writeFile has fs.writeFileSync).

Although the use of these *Sync methods is self-defeating, since you will block the JavaScript event loop while a file operation is performed, these methods can be helpful for cases when they don't interfere with the overall execution logic or you're still in the process of grasping how to structure callbacks. Listing 8-21 illustrates a modified version of the script in listing 8-19 to read a file synchronously.

Listing 8-21. JavaScript-Node JS script to read file synchronously
  
const fs = require('fs');

console.log('Script start');

console.log(fs.readFileSync('index.html', 'utf8'));

console.log('Script end');

The logic in listing 8-21 is very similar to the one in listing-8-19, both import the fs module to read files and then declare a console.log message with a Script start value. The next statement is where the scripts differ, listing 8-21 uses the readFileSync method to synchrnously read a file, which accepts two arguments: the name of the file to read -- index.html -- followed by the encoding of the file -- utf8. Notice the arguments for the readFileSync method are almost identical to the ones used by the readFile method in listing 8-19, the only difference is readFileSync does not use a callback, because it waits until a file is read and outputs the results upon completion, which is the reason the call is wrapped around another console.log message (i.e. to output the results of reading the file). Finally, the script terminates outputting a console log message that says Script end.

Next, place the contents of listing 8-21 in a file named read_file_sync_script.js and place it alongside the index.html from listing 8-18 -- since the script assumes it will be able to read the index.html file -- run the script with Node JS, as shown in listing 8-22.

Listing 8-22. JavaScript script to read file synchronously executed with Node JS
  
[user@laptop]$ node read_file_sync_script.js
Script start
<h1>This is an HTML page for Node JS!</h1>

Script end

The most important aspect of listing 8-22 is the order of the log messages, which is in the same order as the console.log statements in the script. Unlike listing 8-20 which doesn't wait for a file to be read and outputs the contents of a file until the very end, listing 8-22 outputs the contents of the file as the middle step due to the fs.readFileSync method.

While performance wise the difference between listings 8-19/8-20 and listings 8-21/8-22 is negligible, when these differences are applied at a larger scale (e.g. to read very large files, to attend thousands of requests for the same file) their differences can be substantial. To better illustrate this behavior, the last part of this Node JS chapter finishes by exploring the Node JS http module.

The Node JS http module, asynchronous by nature

The Node JS http module is another built-in JavaScript module to perform Hypertext Transfer Protocol (HTTP) operations. HTTP is at the center of Internet activity, as the protocol used by clients (e.g. browsers) to make requests and servers (e.g. web apps) to return responses. In most programming languages (e.g. Python, Java) HTTP operations are synchronous, which means when a client makes a request it has to wait until a response is received, similarly, a server also has to wait to finish responding to one request before it can attend another request. Unless explicit steps are taken (e.g. threading, multiple-processes), both client and server are effectively blocked from doing anything until each one finishes its work.

With JavaScript asynchronous behavior, JavaScript clients (e.g. browsers) have long made use of AJAX to make HTTP requests, while still being able to perform other tasks and not have to freeze/lock-up while a server responds. With the appearance of Node JS, not only is JavaScript running on servers a reality, but the possibility of performing HTTP responses asynchronously (i.e. to handle multiple requests, without needing to finish responses) also becomes a possibility thanks to the Node JS http module.

Listing 8-23 illustrates a basic server that uses the Node JS http module.

Listing 8-23. JavaScript server with Node JS http module
const http = require('http');

// Create a server with the HTTP module:
http.createServer(function (request, response) {
  // Write a string to the response
  response.write('ModernJS Node JS server');
  // End the response
  response.end();
}).listen(3000); // Set the server to list on port 3000

Listing 8-23 starts by importing the http module and making it available through the http reference. Next, a call is made to the createServer() method of the http module to create an HTTP server. Notice the createServer() method uses two arguments -- request and response -- to represent an HTTP request and response. Inside the createServer()response reference using the write() method -- indicating the text to add to all responses made to the server -- and a call is made to the end() method of the response reference to indicate the response is finished. Finally, the listen() method is called on createServer() to configure on which HTTP port the server will run on, in this case, port 3000.

Place the contents in listing 8-23 in a file named myserver.js and run it with node: node myserver.js. Open a browser and visit http://localhost:3000/ or http://127.0.0.1:3000, you'll see the text out output ModerJS Node JS server.

Although this is a very simple server -- with only a single path that always returns the same result -- it illustrates how Node JS is capable of supporting a JavaScript HTTP server in a few lines. Now that you have a basic understanding of how Node JS can function as an HTTP server with the help of the Node JS HTTP Module, let's rework the example in listing 8-23 to be a little more interesting by responding with the contents of a file, which will also showcase one of the subtleties of working asynchronously with HTTP servers.

Listing 8-24 makes use of the Node JS fs module -- presented in listing 8-18 -- to read an HTML file and make the HTTP server respond with its contents. NOTE: This initial iteration is the wrong way to do it, but it's to illustrate a point.

Listing 8-24. JavaScript server with Node JS http module reads data from file (the wrong way)
 
const http = require('http');
const fs = require('fs');

// Create a server with the HTTP module:
http.createServer(function (request, response) {
  // Write a string to the response
  let placeholder_server_response = "This is NOT from a file";  
  fs.readFile('index.html', 'utf8', (error, file_data) => {
      placeholder_server_response=file_data ? file_data: 'Could not read file';
      console.log('The placeholder_server_response is %j', placeholder_server_response )
  });
  response.write(placeholder_server_response);
  // End the response
  response.end();
}).listen(3000); // Set the server to list on port 3000

Listing 8-24 is very similar to listing 8-23, except it creates a place holder variable placeholder_server_response to return on all HTTP responses, as well as reads a file index.html whose contents it attempts to assign to this placeholder variable. Create a file named index.html alongside this new version of myserver.js, run the server and open the page in your browser once again, you'll notice the output will always be the original value from the place holder value This is NOT from a file, do you know why it never returns the contents from the file ?

If you've done any kind of server side programming, you probably expected things in listing 8-24 to run sequentially, that is, for the placeholder_server_response to be overwritten once the file was read and for the contents of this file to be output by the server. However, because Node HTTP JavaScript works asynchronously, it means the workflow doesn't wait for the file to be read -- it moves on immediately -- and therefore placeholder_server_response isn't updated and the response is the original placeholder_server_response value.

This is both the beauty and crux of asynchronous programming: there's no waiting for anyone and so things run more quickly, but you also need to be careful how things are structured so as not to fall in one of these unintended behaviors -- a function not returning the expected data response.

The solution to this problem is to use the callback function, in order for the file reading operation to return the actual HTTP response. Listing 8-25 illustrates the correct way to read a file asynchronously and return the results asynchronously via HTTP.

Listing 8-25. JavaScript server with Node JS http module reads data from file (the right way)
const http = require('http');
const fs = require('fs');

// Create a server with the HTTP module:
http.createServer(function (request, response) {
  // Write a string to the response
  let placeholder_response = "This is NOT from a file";  
    fs.readFile('index.html', 'utf8', (error, file_data) => {
        placeholder_server_response=file_data ? file_data: 'Could not read file';
        console.log('The placeholder_server_response is %j', placeholder_server_response )
	response.write(placeholder_server_response);
	// End the response
	response.end();
    })
}).listen(3000); // Set the server to list on port 3000

If you run the server from listing 8-25, you'll notice the output reflects what you were probably expecting, to see the contents of the index.html file.

With this exploration of the Node JS http module, as well as the other Node JS modules to read files and perform DNS queries, you should have a firm understanding of how everything in Node JS is asynchronous by default -- unless you use special methods to perform tasks synchrnously.

The Node JS node inspect command

.

And with this we conclude the Node JS chapter, showcasing the foundations of this tool that's a staple to Modern JavaScript development.

  1. https://github.com/nodejs/release    

  2. https://nodejs.org/en/download/    

  3. https://github.com/nvm-sh/nvm    

  4. https://v8.dev/docs/version-numbers    

  5. https://nodejs.org/api/modules.html