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.
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:
node
.- A JavaScript interactive shell, also known as read–eval–print loop (REPL), designed to quickly evaluate JavaScript expressions. It functions like other programming language REPLs.npm
. A JavaScript package manager, designed to administer JavaScript packages in Node JS. It functions like other programming language package managers, directly downloading packages from remote repositories or installing/updating/deleting packages from a local Node JS installation.npx
. A complementary tool tonpm
, designed to execute JavaScript packages installed on Node JS.
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:
- The immediately-invoked function expressions (IIFE): Namespaces and block scoping solved, the early years section.
- The
export
&import
keywords and modules: Namespaces solved, the modern years section. - The pre-ES6 (ES2015) modules detour: CommonJS, AMD and UMD section.
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 theglobalThis
reference to access the global object when available (i.e. in JavaScript environments that support this ECMAScript 11 (ES2020) feature).
Although theglobal
andthis
references also provide access to the global object. Theglobal
reference is Node JS specific and thethis
reference is an overly used reference for many other purposes in JavaScript that can get mixed up with other meanings. Therefore, the ECMAScriptglobalThis
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.