Setup
Introduction
In this prelab, you will learn how to write a basic Node.js server and how to manage your dependencies using
npm. Make sure you have Node installed (node --version
should give you the version number of node that is installed on your machine. See Assignment 0 for installation instructions if you do not see a version in your output).
Accessibility/Ethics Readings (Optional)
For the upcoming lab, please take a look at the article Privacy Policies: How Websites Track You, Explained.
Overview
Taken from Wikipedia:Node.js is an open-source, cross-platform, JavaScript runtime environment that executes JavaScript code outside of a browser. Node.js lets developers use JavaScript to write command line tools and for server-side scripting —- running scripts server-side to produce dynamic web page content before the page is sent to the user's web browser . Consequently, Node.js represents a "JavaScript everywhere" paradigm, unifying web-application development around a single programming language, rather than different languages for server-side and client-side scripts.
What concerns us in this quote is that Node.js can evaluate Javascript code outside a browser, just like a Python interpreter. This enables us to write all kinds of applications using Javascript Syntax, including what most relevant to this course -- the server for a web application.
In assignment2 you have written Javascript code that is interpreted by the browser and sends requests. If you have experience with other programming languages, you might have heard or used server-side web-application frameworks like Django and Flask for Python, Ruby on Rails for Ruby, Spring Framework for Java...
In this prelab, we will cover:
- how to run code with Node and the modular system of Node.js
- what is npm and how to install dependencies
- introduce Express , the de facto standard server framework for Node.js.
Node.js
Writing Your First Node.js Application
Create a new directory for this prelab and a new file inside this directory. Name the file anything you want, for example,
server.js
.
Add the following line to the file:
console.log('Hello, world!');
In the terminal, you can run this application with
node server.js
and you should see the phrase
Hello, world!
printed to the command line!
If you type node
in your terminal and click enter, you can access the Node REPL.
Note the string
'Hello, world!'
is printed to the terminal instead of your browser's debugger. In fact,
console
is a global
Console class
instance configured to write to
process.stdout
and
process.stderr
. Node.js actually provides the
console
module which provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.
Modules
Just like any other programming languages, Node becomes much more powerful when you start using modules. In the Node.js module system, each file is treated as a separate module and Node.js has a set of built-in modules.
For example, let us use the
http
module to create a HelloWorld server. Copy the following code from
Node.js About page
to your file:
const http = require('http');
const hostname = '127.0.0.1';
const port = 3000;
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello World');
});
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
Use
node server.js
to run your server, access the printed address in your browser, you should see a plain
Hello World
page.
Use CTRL+C to stop your server.
We will cover more about web server in the
Express
section, now let us focus on the first line
const http = require('http');
.
Importing with require
Node.js has a built-in function
require
that are used to include modules in your current module. These imported modules can be built-in modules like
http
, external modules downloaded by package managers in the
node_modules
directory, or local modules you wrote.
Therefore, the first line will perform a search, find the built-in
http
module, and assigns the exports (explained soon) object to the variable
http
.
The variable name could be anything you want.
Moreover, since the variable is bound to the returned exports object, you can use
Destructuring assignment
to directly bind exported fields to variable names like:
const { createServer } = require('http');
const server = createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello World');
});
Exporting with module.exports
Before a module's code is executed, Node.js will wrap it with a function wrapper that looks like the following:
(function(exports, require, module, __filename, __dirname) {
// Module code actually lives in here
});
By doing this, Node.js achieves a few things:
-
It keeps top-level variables (defined with
var
,const
orlet
) scoped to the module rather than the global object. -
It helps to provide some global-looking variables that are actually specific to the module, such as:
-
The
module
andexports
objects that the implementor can use to export values from the module. -
The convenience variables
__filename
and__dirname
, containing the module's absolute filename and directory path.
-
The
module.exports
is what
require
function actually returns when a module is imported. Since
module.exports
is initially aliased to
exports
(sharing a reference), we can reate another module (file) called, for example,
config.js
and exposes the
hostname
,
port
variables using either
-
exports
exports.hostname = '127.0.0.1'; exports.port = 3000;
-
module.exports
module.exports = { hostname: '127.0.0.1', port: 3000 };
Then in the
server.js
, you can use
const { hostname, port } = require('./config.js');
to import both variables.
Note relative filepath is used rather than only the filename when importing local modules.
This signifies that you are importing a local module instead of a built-in module or downloaded module.
We do not mandate one way over the other. Stylistically speaking, using
module.exports
might lead to better structured code since all exported variables are gathered together. You can read more on the difference between
module.exports
and
exports
from
this StackOverflow post
.
NPM
If you have prior knowledge with package managers like pip for Python, Homebrew for MacOS, it is not hard to imagine how downloading external packages can be tremendously helpful. NPM is a package manager for Node.js modules which gets installed together with Node.
You can use
npm
to install modules from npm's online registry. For example, instead of using those VSCode extensions, you can directly install the linters as packages:
ESLint
,
StyleLint
,
HTMLHint
.
To track downloaded npm packages, it is often a good idea to
creating a package.json
file
. You might have noted we have a
package.json
file in most of our Github repo. This allows you to use
npm install
to download all the dependencies of the stencil without actually store the dependencies concretely in the repo.
Type
npm init
in your terminal at the directory created for this prelab. Feel free to click Enter to accept the defaults. After the interactive prompt is finished, you will find a
package.json
file in your directory.
You can use npm init -y
to create a default package.json
directly.
Now let us try install some modules:
-
Firstly, let us follow the ESLint Getting Started Guide to configure ESLint:
npm install eslint --save-dev
This line can also be shortened to
npm i eslint -D
wherei
is shorthand forinstall
and-D
shorthand for--save-dev
meaning package will be tracked as devDependencies .You can read more about the difference between devDependencies and dependencies at this StackOverflow post . In short, dev dependencies are dependencies that are useful for development purposes, for example, linting the JS files for errors.
This ties to the difference between development environment and production environment, read this post for more information.Set up the ESLint configuration file with
npx eslint --init
wherenpx
is a tool to execute a command exposed by npm packages either from localnode_modules
or from online registry.
Remembernpx serve .
? No magic is happening as serve is actually a npm package! -
Now run
npm install express
to install express -- a web framework for node we will introduce in the next section.
npm install
will not only install the correct module into the
node_modules
directory, but also store the metadata like package name and version in the
package.json
file. For this reason, including the
package.json
file in Git repo or handin is extremely important because it allows other developers to replicate your project by installing all required dependencies themselves rathern than transferring all the dependencies over the network.
You should never include the node_modules
directory in your handin. The package.json
and package-lock.json
files are suffcient for us to install all dependencies in your handins.
Express
Basic Routing
The server you created with
http
is very basic, it neither scales well nor provides commonly required functionalities like cookies, params, template renderings... To build a full-fledged web application server, we will introduce
Express
.
Replace your
server.js
with the following code taken from
Hello world example
in Express.
const express = require('express')
const app = express()
const port = 3000
app.get('/', (req, res) => res.send('Hello World!'))
app.listen(port, () => console.log(`Example app listening on port ${port}!`))
Run the server with
node server.js
, go to the address at
http://localhost:3000
and you should see
Hello World
being printed out.
- At line 2, we are creating an express app. Think this as an instance of the express server.
-
At line 7, we tell the express app to listens on the port 3000 for connections. Since we are running the server locally and, in computer networking,
localhost
is a hostname that means this computer, we can access the express app at http://localhost:3000 . -
Try a little experimentation, what if you access a specific endpoint (route) like http://localhost:3000/random , what will your express server respond?
At line 5, we register the root route
/
in the express app. More specifically, we are telling the express app that if the user visits the root route via GET method, execute the callback which is the second argument.
Remember visiting the URL automatically triggers a GET request and visiting http://localhost:3000 is the same as visiting http://localhost:3000/ .Since only the root route is registered, the express app does not know how to handle other routes like
/random
. By default, it will respond with a 404 Not Found. If you open the browser inspector Network tab and refresh the page, you can see the RESPONSE status is 404 Not Found .app.get
means the express application will respond to the client request at the specified endpoint (the first argument which is often called route ) using the specified callback (the second argument which is often called handler ) when GET request method is used.
Similarly, you can use register handlers for other HTTP request method like POST .
app.post('/', function (req, res) { res.send('Got a POST request'); });
Restart your server and you can simulate a POST request from terminal using curl:
curl --request POST --url http://localhost:3000/
Route Paths
Besides specifying a string for the route registration, you can also use string patterns and regular expressions to specify a set of routes.
For example, we can use the wildcard
*
to match all routes:
app.get('*', (req, res) => res.sendStatus(404));
*
means matching all. Therefore, the registered callback will be executed for every route if it is not already handled.
Add this line after your registration for GET, POST methods on the root routes. The order is significant, try putting this line before, restart the server and observe what happens when you visit the root route.
If we put this catchall route earlier than specific routes, every route is first caught by the catchall handler which terminates processing and the callbacks at specific routes will never be triggered.
By registering the catchall handler last, we are matching the remaining routes.
res.sendStatus
is a method on the response object (the second argument on the callback) that set the
response status code
and send its string representation as the response body.
Tasks:
Serving static files in Express
Web applications usually have static files such as images, CSS files, and JavaScript files. You do not need to manually create a route for each file, rather, you can host an entire static directory with express.static .
-
Create a
public
directory -
Within the public directory, create a
images
directory -
Put your favorite image inside the
images
directory. For example, I used a ratatouille photo . -
Add the following code to serve images, CSS files, and JavaScript files in a directory named public:
app.use(express.static('public'))
-
Restart the server and go to the corresponding route, for example, I put a
ratatouille.jpg
inpublic/images
and I can access it athttp://localhost:3000/images/ratatouille.jpg
.
You might have noticed we used
app.use
rather than a specific HTTP method name like
app.get
.
app
means the effect is application-level: we are specifying something global here and
use
means a
middleware
is used as the callback function.
This is the end of the prelab! We have only covered the basics for Node.js so if you're confused about anything, feel free to look at official documentations or come to TA Hours.
Handin Instructions
main
branch.