035.1 Lesson 1
Certificate: |
Web Development Essentials |
---|---|
Version: |
1.0 |
Topic: |
035 Node.js Server Programming |
Objective: |
035.1 Node.js Basics |
Lesson: |
1 of 1 |
Introduction
Node.js is a JavaScript runtime environment that runs JavaScript code in web servers–the so-called web backend (server side)–instead of using a second language like Python or Ruby for server-side programs. The JavaScript language is already used in the modern frontend side of web applications, interacting with the HTML and CSS of the interface that the user interacts with a web browser. Using Node.js in tandem with JavaScript in the browser offers the possibility of just one programming language for the whole application.
The main reason for the existence of Node.js is the way it handles multiple concurrent connections in the backend. One of the most common ways that a web application server handles connections is through the execution of multiple processes. When you open a desktop application in your computer, a process starts and uses a lot of resources. Now think about when thousands of users are doing the same thing in a large web application.
Node.js avoids this problem using a design called the event loop, which is an internal loop that continously checks for incoming tasks to be computed. Thanks to the widespread use of JavaScript and the ubiquity of web technologies, Node.js has seen a huge adoption in both small and large applications. There are other characteristics that also helped Node.js to be widely adopted, such as asynchronous and non-blocking input/output (I/O) processing, which is explained later in this lesson.
The Node.js environment uses a JavaScript engine to interpret and execute JavaScript code on the server side or on the desktop. In these conditions, the JavaScript code that the programmer writes is parsed and compiled just-in-time to execute the machine instructions generated by the original JavaScript code.
Note
|
As you progress through these lessons about Node.js, you may notice that the Node.js JavaScript is not exactly the same as the one that runs on the browser (which follows the ECMAScript specification), but is quite similar. |
Getting Started
This section and the following examples assume that Node.js is already installed on your Linux operating system, and that the user already has basic skills such as how to execute commands in the terminal.
To run the following examples, create a working directory called node_examples
. Open a terminal prompt and type node
. If you have correctly installed Node.js, it will present a >
prompt where you can test JavaScript commands interactively. This kind of environment is called REPL, for “read, evaluate, print, and loop”. Type the following input (or some other JavaScript statements) at the >
prompts. Press the Enter key after each line, and the REPL environment will return the results of its actions:
> let array = ['a', 'b', 'c', 'd']; undefined > array.map( (element, index) => (`Element: ${element} at index: ${index}`)); [ 'Element: a at index: 0', 'Element: b at index: 1', 'Element: c at index: 2', 'Element: d at index: 3' ] >
The snippet was written using ES6 syntax, which offers a map function to iterate over the array and print the results using string templates. You can write pretty much any command that is valid. To exit the Node.js terminal, type .exit
, remembering to include the initial period.
For longer scripts and modules, it is more convenient to use a text editor such as VS Code, Emacs, or Vim. You can save the two lines of code just shown (with a little modification) in a file called start.js
:
let array = ['a', 'b', 'c', 'd'];
array.map( (element, index) => ( console.log(`Element: ${element} at index: ${index}`)));
Then you can run the script from the shell to produce the same results as before:
$ node ./start.js Element: a at index: 0 Element: b at index: 1 Element: c at index: 2 Element: d at index: 3
Before diving into some more code, we are going to get an overview of how Node.js works, using its single thread execution environment and the event loop.
Event Loop and Single Thread
It is hard to tell how much time your Node.js program will take to handle a request. Some requests may be short—perhaps just looping through variables in memory and returning them—whereas others may require time-consuming activities such as opening a file on the system or issuing a query to a database and waiting for the results. How does Node.js handle this uncertainty? The event loop is the answer.
Imagine a chef doing multiple tasks. Baking a cake is one task that requires a lot of time for the oven to cook it. The chef does not stay there waiting for the cake to be ready and then set out to make some coffee. Instead, while the oven bakes the cake, the chef makes coffee and other tasks in parallel. But the cook is always checking whether it is the right time to switch focus to a specific task (making coffee), or to get the cake out of oven.
The event loop is like the chef who is constantly aware of the surrounding activities. In Node.js, an “event-checker” is always checking for operations that have completed or are waiting to be processed by the JavaScript engine.
Using this approach, an asynchronous and long operation does not block other quick operations that come after. This is because the event loop mechanism is always checking whether that long task, such as an I/O operation, is already done. If not, Node.js can continue to process other tasks. Once the background task is complete, the results are returned and the application on top of Node.js can use a trigger function (callback) to further process the output.
Because Node.js avoids the use of multiple threads, as other environments do, it is called a single-threaded environment, and therefore a non-blocking approach is of the utmost importance. This is why Node.js uses an event loop. For compute-intensive tasks, Node.js is not among the best tools, however: there are other programming languages and environments that address these problems more efficiently.
In the following sections, we will take a closer look at callback functions. For now, understand that callback functions are triggers that are executed upon the completion of a predefined operation.
Modules
It is a best practice to break down complex functionality and extensive pieces of code into smaller parts. Doing this modularization helps to better organize the codebase, abstract away the implementations, and avoid complicated engineering problems. To meet those necessities, programmers package blocks of source code to be consumed by other internal or external parts of code.
Consider the example of a program that calculates the volume of a sphere. Open up your text editor and create a file named volumeCalculator.js
containing the following JavaScript:
const sphereVol = (radius) => {
return 4 / 3 * Math.PI * radius
}
console.log(`A sphere with radius 3 has a ${sphereVol(3)} volume.`);
console.log(`A sphere with radius 6 has a ${sphereVol(6)} volume.`);
Now, execute the file using Node:
$ node volumeCalculator.js A sphere with radius 3 has a 113.09733552923254 volume. A sphere with radius 6 has a 904.7786842338603 volume.
Here, a simple function was used to compute the volume of a sphere, based on its radius. Imagine that we also need to calculate the volume of a cylinder, cone, and so on: we quickly notice that those specific functions must be added to the volumeCalculator.js
file, which can become a huge collection of functions. To better organize the structure, we can use the idea behind modules as packages of separated code.
In order to do that, create a separated file called polyhedrons.js
:
const coneVol = (radius, height) => {
return 1 / 3 * Math.PI * Math.pow(radius, 2) * height;
}
const cylinderVol = (radius, height) => {
return Math.PI * Math.pow(radius, 2) * height;
}
const sphereVol = (radius) => {
return 4 / 3 * Math.PI * Math.pow(radius, 3);
}
module.exports = {
coneVol,
cylinderVol,
sphereVol
}
Now, in the volumeCalculator.js
file, delete the old code and replace it with this snippet:
const polyhedrons = require('./polyhedrons.js');
console.log(`A sphere with radius 3 has a ${polyhedrons.sphereVol(3)} volume.`);
console.log(`A cylinder with radius 3 and height 5 has a ${polyhedrons.cylinderVol(3, 5)} volume.`);
console.log(`A cone with radius 3 and height 5 has a ${polyhedrons.coneVol(3, 5)} volume.`);
And then run the filename against the Node.js environment:
$ node volumeCalculator.js A sphere with radius 3 has a 113.09733552923254 volume. A cylinder with radius 3 and height 5 has a 141.3716694115407 volume. A cone with radius 3 and height 5 has a 47.12388980384689 volume.
In the Node.js environment, every source code file is considered a module, but the word “module” in Node.js indicates a package of code wrapped up as in the previous example. By using modules, we abstracted the volume functions away from the main file, volumeCalculator.js
, thus reducing its size and making it easier to apply unit tests, which are a good practice when developing real world applications.
Now that we know how modules are used in Node.js, we can use one of the most important tools: the Node Package Manager (NPM).
One of the main jobs of NPM is to manage, download, and install external modules into the project or in the operating system. You can initialize a node repository with the command npm init
.
NPM will ask the default questions about the name of your repository, version, description, and so on. You can bypass these steps using npm init --yes
, and the command will automatically generate a package.json
file that describes the properties of your project/module.
Open the package.json
file in your favorite text editor and you will see a JSON file containing properties such as keywords, script commands to use with NPM, a name, etc.
One of those properties is the dependencies that are installed in your local repository. NPM will add the name and version of these dependencies into package.json
, along with package-lock.json
, another file used as fallback by NPM in case package.json
fails.
Type the following in your terminal:
$ npm i dayjs
The i
flag is a shortcut for the argument install
. If you are connected to the internet, NPM will search for a module named dayjs
in the remote repository of Node.js, download the module, and install it locally. NPM will also add this dependency to your package.json
and package-lock.json
files. Now you can see that there is a folder called node_modules
, which contains the installed module along with other modules if they are needed. The node_modules
directory contains the actual code that is going to be used when the library is imported and called. However, this folder is not saved in versioning systems using Git, since the package.json
file provides all the dependencies used. Another user can take the package.json
file and simply run npm install
in their own machine, where NPM will create a node_modules
folder with all the dependencies in the package.json
, thus avoiding version control for the thousands of files available on the NPM repository.
Now that the dayjs
module is installed in the local directory, open the Node.js console and type the following lines:
const dayjs = require('dayjs');
dayjs().format('YYYY MM-DDTHH:mm:ss')
The dayjs
module is loaded with the require
keyword. When a method from the module is called, the library takes the current system datetime and outputs it in the specified format:
2020 11-22T11:04:36
This is the same mechanism used in the previous example, where the Node.js runtime loads the third party function into the code.
Server Functionality
Because Node.js controls the back end of web applications, one of its core tasks is to handle HTTP requests.
Here is a summary of how web servers handled incoming HTTP requests. The functionality of the server is to listen for requests, determine as quickly as possible what response each needs, and return that response to the sender of the request. This application must receive an incoming HTTP request triggered by the user, parse the request, perform the calculation, generate the response, and send it back. An HTTP module such as Node.js is used because it simplifies those steps, alowing a web programmer to focus on the application itself.
Consider the following example that implements this very basic functionality:
const http = require('http');
const url = require('url');
const hostname = '127.0.0.1';
const port = 3000;
const server = http.createServer((req, res) => {
const queryObject = url.parse(req.url,true).query;
let result = parseInt(queryObject.a) + parseInt(queryObject.b);
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end(`Result: ${result}\n`);
});
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
Save these contents in a file called basic_server.js
and run it through a node
command. The terminal running Node.js will display the following message:
Server running at http://127.0.0.1:3000/
Then visit the following URL in your web browser: http://127.0.0.1:3000/numbers?a=2&b=17
Node.js is running a web server in your computer, and using two modules: http
and url
. The http
module sets up a basic HTTP server, processes the incoming web requests, and hands them to our simple application code. The URL module parses the arguments passed in the URL, converts them into an integer format, and performs the addition operation. The http
module then sends the response as text to the web browser.
In a real web application, Node.js is commonly used to process and retrieve data, usually from a database, and return the processed information to the front end to display. But the basic application in this lesson concisely shows how Node.js makes use of modules to handle web requests as a web server.
Guided Exercises
-
What are the reasons to use modules instead of writing simple functions?
-
Why did the Node.js environment become so popular? Cite one characteristic.
-
What is the purpose of the
package.json
file? -
Why is it not recommended to save and share the
node_modules
folder?
Explorational Exercises
-
How can you execute Node.js applications on your computer?
-
How can you delimit parameters in the URL to parse inside the server?
-
Specify a scenario where a specific task could be a bottleneck for a Node.js application.
-
How would you implement a parameter to multiply or sum the two numbers in the server example?
Summary
This lesson provided an overview of the Node.js environment, its characteristics, and how it can be used to implement simple programs. This lesson includes the following concepts:
-
What Node.js is, and why it is used.
-
How to run Node.js programs using the command line.
-
The event loops and the single thread.
-
Modules.
-
Node Package Manager (NPM).
-
Server functionality.
Answers to Guided Exercises
-
What are the reasons to use modules instead of writing simple functions?
By opting for modules instead of conventional functions, the programmer creates a simpler codebase to read and maintain and for which to write automated tests.
-
Why did the Node.js environment become so popular? Cite two characteristics.
One reason is the flexibility of the JavaScript language, which was already widely used in the front end of web applications. Node.js allows the use of just one programming language in the whole system.
-
What is the purpose of the
package.json
file?This file contains metadata for the project, such as the name, version, dependencies (libraries), and so on. Given a
package.json
file, other people can download and install the same libraries and run tests in the same way that the original creator did. -
Why is it not recommended to save and share the
node_modules
folder?The
node_modules
folder contains the implementations of libraries available in remote repositories. So the better way to share these libraries is to indicate them in thepackage.json
file and then use NPM to download those libraries. This method is simpler and more error-free, because you do not have to track and maintain the libraries locally.
Answers to Explorational Exercises
-
How can you execute Node.js applications on your computer?
You can run them by typing
node PATH/FILE_NAME.js
at the command line in your terminal, changingPATH
to the path of your Node.js file and changingFILE_NAME.js
to your chosen filename. -
How can you delimit parameters in the URL to parse inside the server?
The ampersand character
&
is used to delimit those parameters, so that they can be extracted and parsed in the JavaScript code. -
Specify a scenario where a specific task could be a bottleneck for a Node.js application.
Node.js is not a good environment in which to run CPU intensive processes because it uses a single thread. A numerical computation scenario could slow down and lock the entire application. If a numerical simulation is needed, it is better to use other tools.
-
How would you implement a parameter to multiply or sum the two numbers in the server example?
Use a ternary operator or an if-else condition to check for an additional parameter. If the parameter is the string
mult
return the product of the numbers, else return the sum. Replace the old code with the snippet below. Restart the server in the command line by pressing kbd:[Ctrl+C] and rerunning the command to restart the server. Now test the new application by visitiing the URLhttp://127.0.0.1:3000/numbers?a=2&b=17&operation=mult
in your browser. If you omit or change the last parameter, the results should be the sum of the numbers.let result = queryObject.operation == 'mult' ? parseInt(queryObject.a) * parseInt(queryObject.b) : parseInt(queryObject.a) + parseInt(queryObject.b);