Introduction to Node
February 10, 2020
The goals of this lecture are:
- Describe what AJAX is
- Compare AJAX requests to non-AJAX requests
- Make GET and POST AJAX requests with axios
- Use async / await to manage asynchronous code with axios
- 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.
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.
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
.
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:
- Access environmental variables
- See the command line arguments that were passed to the script
- 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.
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
.
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:
- Show a message in the DOM
- Pop up an alert dialog
- Log error to the console
But within Node, we'll handle them one of two ways:
- Log the error to the console
- Exit the process using
process.exit(1)
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:
- path The relative path to the file we'd like to read.
- encoding How the file should be interpreted
- Text files are almost always interpreted as
utf8
- Binary files (like images), this argument can be omitted
- 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:
- path The relative path to the file we'd like to write to
- data Data to write to the file (usually a string)
- encoding How to write the file
- Text files are almost always interpreted as
utf8
- Binary files (like images), this argument can be omitted
- 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:
- The global object isn't
window
(we're not in the browser!), but rather an object calledglobal
. - There are no
document
or DOM methods. - We have the ability to access the file system and can start server processes.