Introduction to Node

February 10, 2020

This article is a written version of Rithm School’s Introduction to Node lecture.

The goals of this lecture are:

  1. Describe what AJAX is
  2. Compare AJAX requests to non-AJAX requests
  3. Make GET and POST AJAX requests with axios
  4. Use async / await to manage asynchronous code with axios
  5. Describe what JSON is

What is Node.js?

Node.js (most often referred to simply as "Node"), is a JavaScript runtime environment that executes JavaScript outside of the browser.

Node can be used to build both servers for web applications and also be used as a general-purpose scripting language.

Why Node?

If given the choice, we might select Node for our server because it allows us to write JavaScript both on the frontend and the backend.

In addition. Node is widely adopted and has a robust community behind it, meaning we have access to an extensive collection of additional libraries and utilities that we can easily incorporate into our project.

Starting Node

To run a program writting in Node, we use the node command in our terminal, followed by the program's filename:

$ node myScript.js
Hi there!

If we'd like to enter the interactive shell, to write JavaScript in our Node environment in real time, we just use the node command:

$ node 
>

NPM (Node Package Manager)

Hand-in-hand with Node is npm, a popular software repository that holds hundreds of thousands of libraries available for download and use in Node.

npm is not only used to refer to the repository of libraries itself, but also to the command line tool that allows us to interact with and download libraries from the repository.

Creating a Node Project

To create a new project with npm, we run the following command:

$ npm init

The init command will initialize a new Node program, creating a configuration file with the name of package.json that will contain both metadata and dependency information for our entire project.

💡Running just npm init will fire off a series of yes/no questions about settings for our newly created project. If we'd like to quickly accept all default values, we can include the flag --yes (npm init --yes).

Installing Node Packages

To install one of the many available packages offered through npm, we run the npm install command in the root of our project.

$ npm install axios

In the case above, we're reaching out to npm to download the axios package that's available.

Besides downloading the package to our project's node_modules directory, the latest version of axios will then be added as a value to the dependencies key in our package.json file.

The node_modules directory holds all of the packages that the project is dependent on.

💡Always add the node_modules directory to your `.gitignore` file. Since our package.json file keeps a record of our dependencies that we can easily reinstall, there is no need to keep the actual downloads of these packages in our repository. Doing so causes the repository to balloon in size unnecessarily.

Installing Dependencies from Existing Project

In the case that we pull down a project that we do not already have locally, to install the dependencies used by the project, we make use of the package.json file.

In the root of the directory, where package.json lives, we run:

$ npm install

This command will cause npm to read the package.json file, identify the dependencies used, and install them in the node_modules directory.

If we think back to our work in Python, this is similar to our previous use of the command pip install -r requirements.txt.

💡When we npm install using package.json, a new file is created and/or updated: package-lock.json. This file holds a record of the exact versions of each dependency that the project requires.

Node process Object

When using Node, we have access to a global object named process that gives us information and control over the current script (or process) that is being executed.

With process, we can:

  1. Access environmental variables
  2. See the command line arguments that were passed to the script
  3. Kill the current script/process
process.env

One of the properties available on the process object that we'll use frequently is env, short for environment.

This object will contain any environmental variables that our program has available to it.

To access environmental variables from our shell:

$ export SECRET_INFO=abc123
$ node
> process.SECRET_INFO
'abc123'
process.argv

Another property off of process that we'll become more familiar with is that of argv.

This propery (an array) will contain any arguments that were passed to our script when it was run.

Let's say that we run the following command in our shell:

$ node showArgs.js hello world

In the case above, we are using Node to run the program contained in the file showArgs.js. Additionally, we are sending in two additional arguments: hello and world.

If we were to print out each of the members of process.argv in this case, we would see the following:

'/path/to/node'
'/path/to/showArgs.js'
'hello'
'world'

There we see not only our hello and world arguments, but also the two other arguments in the command we ran: node (the argument being the filepath to Node), as well as the path to the script we're running.

process.exit(exit_code)

Last but not least, we have process.exit, which will allow us to exit our script with an exit code of our choice.

The exit code returned represents the condition in which the program ended. Traditionally, 0 corresponds to no error, whereas other numbers indicate some sort of script error occurred.

The Module System

Within the Node ecosystem, each file created is treated as a separate module.

A module can be thought of a self-contained bundle of code, made up of functions, variables and objects that we can selectively decide to share with other files within our application.

Since there are no <script> tags for us to include other JavaScript files, importing other modules will be how we share code across our Node applications.

💡You may hear the name CommonJS used to describe the module system in Node, which is the system that Node has relied on since before ES Modules came into the picture.
Importing Modules

To import a module in Node, we use the require() function, sending into it the relative path of the imported module's file:

const usefulStuff = require("./usefulStuff");
const results = usefulStuff.add(2, 3);

console.log(results);

As you can see, we've omitted the file extension (.js) of our usefulStuff.js filename. For .js and .json files, we do not need to include the extension.

./ indicates that we'd like to look in our current directory, whereas ../ will look up a level for the module in question.

Importing Libraries

In the case that we want to import a package dependency (those libraries located in our node_modules directory), we do not include the relative path as we do with our own files:

const axios = require('axios');

axios.get('http://google.com').then(function(resp) {
  console.log(resp.data.slice(0, 80), '...');
});

In the case above, we want to use the axios library. To instruct Node to look in our node_modules directly, we simply provide it the package name.

Destructuring Imports

When importing modules or libraries, we can use ES6 to destructure what's given to ensure that we are only using what we need:

const { add, User } = require('./usefulStuff');
const results = add(2, 3);

console.log(results);

In the example above, we are destructuring the module from the file usefulStuff.js, specifically selecting the add function and User class for use in our current file.

Exporting from a File

To export variables, functions and objects for use outside of our modules, we will use the built-in module.exports property.

In the example below, we're declaring various data structures, including MY_GLOBAL, add, User and notNeededElsewhere.

At the bottom of our module, using module.exports, we're selectively exporting only three of them (MY_GLOBAL, add() and User):

const MY_GLOBAL = 42;

function add(x, y) {
  return x + y;
}

class User {
  constructor(name, username) {
    this.name = name;
    this.username = username;
  }
}

const notNeededElsewhere = 'nope'; // I don't get exported!

// export an object
module.exports = {
  MY_GLOBAL: MY_GLOBAL,
  add: add,
  User: User
};

Even if we imported the entire module into another file, we would not have access to any of the content that we did not export using module.exports.

💡Normally, module.exports is an object which we access the exposed module members as properties. However, we could set module.exports to anything (a function, for example).

Node Callbacks

Many libraries in Node make use of asynchronous callbacks by default. A callback is the name given to a function that will be called after some process has completed.

Take the example below:

fs.readFile('myFile.txt', 'utf8', function(err, data) {
    // process file here...
});

In the above case, we are using the method .readFile, passing into it several arguments. The last argument we're passing in is a callback.

When the process of reading the file myFile.txt has completed, that callback will be run.

Error-first Callbacks

It's an established pattern in Node that when it comes to our callback functions, the first parameter corresponds to any errors that have arisen while trying to complete the function.

If an error occurs during the execution of the callback, Node supplies us with an error object, otherwise the error parameter will be null.

We will write our functions to handle this error first, before proceeding:

fs.readFile("myFile.txt", "utf8", function(err, data) {
  if (err) {
    // handle error
  }
  // otherwise we're good
});
Handling Errors

From our work in the browser, we have seen several ways to handle errors that occur:

  1. Show a message in the DOM
  2. Pop up an alert dialog
  3. Log error to the console

But within Node, we'll handle them one of two ways:

  1. Log the error to the console
  2. Exit the process using process.exit(1)
💡The 1 in process.exit(1) means Uncaught Fatal Exception. Check the Node documentation for all possible error codes!

File System Module

Node has a built-in module called fs that allows us to access and work with files on the local file system.

Usually, we will use fs to both read and write to files.

To get started, we need to do nothing more than require it and set it to a variable:

const fs = require('fs');
Reading Files

To read a file's content with fs, we use the readFile method:

fs.readFile(path, encoding, callback)

The following is a breakdown of each argument that readFile accepts:

  1. path The relative path to the file we'd like to read.
  2. encoding How the file should be interpreted
  3. Text files are almost always interpreted as utf8
  4. Binary files (like images), this argument can be omitted
  5. callback A function, executed after readFile completes
const fs = require('fs');

fs.readFile('myFile.txt', 'utf8', function(err, data) {
  if (err) {
    // handle possible error
    console.error(err);
    // kill the process and tell the shell it errored
    process.exit(1);
  }
  // otherwise success
  console.log(`file contents: ${data}`);
});

console.log('reading file');
// file won't have been read yet at this point

In the case above, we're using fs to read our myFile.txt file. Using utf8 as the encoding, when the file has been read, we are passing the file contents (stored in the variable data of our callback) and logging them out to the console.

Writing Files

To write to a file, we use the fs method writeFile:

fs.writeFile(path, data, encoding, callback)

Like readFile, there are multiple arguments available to us, most of which are identical to what we've already seen:

  1. path The relative path to the file we'd like to write to
  2. data Data to write to the file (usually a string)
  3. encoding How to write the file
  4. Text files are almost always interpreted as utf8
  5. Binary files (like images), this argument can be omitted
  6. callback A function, executed after writeFile completes

Let's take a look at an example of writeFile:

const fs = require('fs');

const content = 'THIS WILL GO IN THE FILE!';

fs.writeFile('./files/output.txt', content, "utf8", function(err) {
  if (err) {
    console.error(err);
    process.exit(1);
  }
  console.log('Successfully wrote to file!');
});

console.log('writing file...');
// file won't have been written yet at this point

In the case above, we're using fs to write to a file named output.txt. Using utf8 as the encoding, we are passing a string (stored in the variable content) to be written to the file. When the write has completed, we're logging a message, indicating it was successful.

Node vs. JavaScript in Browser

Up until this point, the JavaScript that we've dealt with has been strictly on the browser side of things.

Because Node uses the same underlying engine as the browser to execute our JavaScript, most programmatic behavior is exactly the same.

That said, there are a few differences in Node compared to what we've seen so far:

  1. The global object isn't window (we're not in the browser!), but rather an object called global.
  2. There are no document or DOM methods.
  3. We have the ability to access the file system and can start server processes.