Difference between revisions of "Node.JS"

From CSE330 Wiki
Jump to navigationJump to search
 
(11 intermediate revisions by 2 users not shown)
Line 1: Line 1:
−
AJAX is a useful technology for web applications that need to pull periodic updates from your server. But what do you do when you have thousands of clients all requesting data that changes real-time? A PHP+AJAX approach could choke without a lot of effort on your part.  It is situations like these that Node.js comes to the rescue. Node.js is a web framework, made in JavaScript, that's become extremely popular over the last few years.
+
AJAX is a useful technology for web applications that need to pull periodic updates from your server. But what do you do when you have thousands of clients all requesting data that changes in real-time? A PHP+AJAX approach could struggle without a lot of effort on your part.
  
−
In general, JavaScript frameworks are very trendy right now; it can be hard to keep up with the new cool thing. In fact, there's a joke website https://dayssincelastjavascriptframework.com/, that "tracks" how long it's been since a new framework has come out.
+
It is situations like these that Node.js comes to the rescue.
 +
 
 +
Node.js is a '''JavaScript runtime''' (built on Chrome's V8 engine) that lets you run JavaScript outside the browser. Frameworks like Express run ''on top'' of Node.js.
 +
 
 +
In general, JavaScript frameworks are trendy; it can be hard to keep up with the new cool thing. In fact, there's a joke website https://dayssincelastjavascriptframework.com/ that "tracks" how long it's been since a new framework has come out.
  
 
== Non-Blocking I/O ==
 
== Non-Blocking I/O ==
  
−
You dabbled in the concept of non-blocking I/O when you implemented AJAX in the previous JavaScript module. Node.js uses this execution model at its core.
+
You dabbled in the concept of non-blocking I/O when you implemented AJAX in the previous JavaScript module. Node.js uses this execution model at its core.
 +
 
 +
A simple way to think about Node:
 +
 
 +
* Node is designed to handle lots of connections efficiently.
 +
* Instead of waiting for slow operations (network, file system, database) to finish, Node starts the work and continues doing other things.
 +
* When the slow operation finishes, Node runs some code to handle the result.
  
−
The core idea is the use of '''''anonymous functions''''' as ''callbacks'' for processing the results of intensive operations. Consider the pseudo-code:
+
Historically, the core idea was the use of '''''anonymous functions''''' as ''callbacks'' for processing the results of intensive operations. Consider the pseudo-code:
  
 
<source lang="javascript">
 
<source lang="javascript">
Line 16: Line 26:
 
</source>
 
</source>
  
−
The above pseudo-code queries a database, saves the result of the query in a variable named ''data'', and then prints out the first title in ''data''. The problem arises when multiple clients all want to access the server at the same time. ''In a blocking I/O model, a client needs to wait until all other clients are finished before it can start its process.''  With intensive operations and thousands of clients, this can easily crash your server!
+
The above pseudo-code queries a database, saves the result of the query in a variable named ''data'', and then prints out the first title in ''data''. The problem arises when multiple clients all want to access the server at the same time.
  
−
The solution is to use ''non-blocking I/O''. Consider this revised pseudo-code:
+
''In a blocking I/O model, a client needs to wait until all other clients are finished before it can start its process.'' With intensive operations and thousands of clients, this can easily crash your server (or force you to provision much more hardware than you want).
 +
 
 +
The solution is to use ''non-blocking I/O''. Consider this revised pseudo-code:
  
 
<source lang="javascript">
 
<source lang="javascript">
Line 28: Line 40:
 
</source>
 
</source>
  
−
The revised pseudo-code achieves the same end result as the first version. However, ''it allows other clients to perform tasks while this process is waiting for the database to be queried!'' The code occurs in two parts: first, we start the query. Second, ''when the query is complete'', our '''anonymous callback function''' is executed. In Node.js, callback functions are usually passed one or more parameters.
+
The revised pseudo-code achieves the same end result as the first version. However, ''it allows other clients to perform tasks while this process is waiting for the database to be queried!''
 +
 
 +
The code occurs in two parts:
 +
 
 +
# Start the query.
 +
# ''When the query is complete,'' run the callback function.
 +
 
 +
In Node.js, callback functions are usually passed one or more parameters.
 +
 
 +
=== Modern Node style: Promises and async/await ===
 +
 
 +
Callbacks are still important to understand (they explain why Node "works"), but modern Node code often uses '''Promises''' and '''async/await''' because they make programs easier to read and debug.
 +
 
 +
Conceptually:
 +
 
 +
<source lang="javascript">
 +
// Promise + async/await pseudo-code
 +
 
 +
const data = await database.query("select * from news");
 +
console.log(data[0].title);
 +
</source>
 +
 
 +
This is still non-blocking behind the scenes—it's just nicer syntax for “start work now, continue later when it finishes.”
 +
 
 +
=== One thread? Concurrency? ===
 +
 
 +
You will often hear that Node “runs in one thread.” The key point for this course is:
 +
 
 +
* You can handle many concurrent connections without making a thread per client.
 +
* The server remains responsive because it avoids blocking on slow I/O.
  
−
Better yet, with non-blocking I/O, you have no concurrency issues, because everything runs in just one thread. Nifty!
+
(There are deeper details in the real Node event loop model, but the above understanding will serve you well for CSE 3300.)
  
 
== Installing Node.js ==
 
== Installing Node.js ==
  
−
Run the following commands to install Node and NPM:
+
We will install Node using '''nvm''' (Node Version Manager). This makes it easy to switch Node versions and consistently use the current LTS version.
 +
 
 +
Run the following commands:
  
 
<source lang="bash">
 
<source lang="bash">
−
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
+
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash
−
-
 
 
$ source ~/.bashrc
 
$ source ~/.bashrc
−
-
+
$ nvm install --lts
−
$ nvm install 16
+
$ node -v
 +
$ npm -v
 
</source>
 
</source>
  
 +
'''Note:''' As of October 2026, Node 24 is the Active LTS line. Using `nvm install --lts` means you do not have to hardcode a specific major version in the instructions.
 +
 +
== Your First Node.js Application: Hello World ==
 +
 +
Let's try the classic HTTP server example.
 +
 +
=== Modern version (ES Modules) ===
 +
 +
Create a file named `hello.js`:
 +
 +
<source lang="javascript">
 +
import http from "node:http";
  
−
<!-- The unofficial instructions for installing Node.js can be found here: https://gist.github.com/isaacs/579814
+
http.createServer((req, res) => {
 +
res.writeHead(200, { "Content-Type": "text/plain" });
 +
res.end("Hello World\n");
 +
}).listen(3456);
  
−
There are several choices, but my favorite is probably ''use-nave-no-shell.sh''.
+
console.log("Server running at http://localhost:3456/");
 +
</source>
  
−
The instructions at the above link have a couple bugs.  If you run this sequence of commands, Node and NPM will install correctly:
+
To run it:
  
 
<source lang="bash">
 
<source lang="bash">
−
$ mkdir ~/.nave
+
$ node hello.js
−
$ cd ~/.nave
 
−
$ wget http://github.com/isaacs/nave/raw/master/nave.sh
 
−
$ sudo ln -s $PWD/nave.sh /usr/local/bin/nave
 
−
$ chmod a+x nave.sh
 
−
$ sudo chown -R $USER:$USER /usr/local
 
−
$ nave usemain stable
 
−
$ curl -L https://www.npmjs.org/install.sh | sh
 
 
</source>
 
</source>
  
−
-->
+
'''Note:''' If you're running the code from your EC2 instance, you will need to open port 3456.
 +
 
 +
=== Enabling ES Modules (important!) ===
  
−
== Your First Node.js Application: Hello World ==
+
Modern Node supports both the older '''CommonJS''' module system (require) and the newer JavaScript standard '''ES Modules''' (import/export). ES Modules are the direction modern JavaScript has moved (and they match browser JavaScript).
  
−
Let's try running the example application given on the front page of nodejs.org:
+
To tell Node to treat `.js` files as ES Modules, add this to `package.json` in your project directory:
  
 
<source lang="javascript">
 
<source lang="javascript">
−
const http = require('http');
+
{
−
http.createServer(function (req, res) {
+
  "type": "module"
−
res.writeHead(200, {
+
}
−
'Content-Type': 'text/plain'
 
−
});
 
−
res.end('Hello World\n');
 
−
}).listen(3456);
 
−
console.log('Server running at http://localhost:3456/');
 
 
</source>
 
</source>
  
−
Paste the example into a file named `hello.js`, and then run it by calling `node hello.js`. You should now be able to see your Hello World by visiting your server at port 3456!  (Press ^C to stop the application.)  '''Note:''' If you're running the code from your EC2 instance, you will need to open port 3456.
+
If you do not do this, Node may treat `.js` files as CommonJS by default, and `import ...` may fail.
  
−
Let's walk through each line in the above example.
+
=== Walkthrough of the Hello World example ===
  
−
* <code>var http = require('http');</code> enables us to use Node's built-in HTTP server. We've been spoiled that languages like PHP automatically load all of the libraries that we need.  However, '''''most functionality in Node.js is NOT loaded by default.'''''  When you want to use any non-fundamental feature of Node, you need to ''require'' it into a variable, as this example does with the HTTP package. You should be familiar with this concept when you needed to use ArrayList and other specialty packages in Java in CSE 132.
+
* <code>import http from "node:http";</code> enables us to use Node's built-in HTTP server library. Unlike PHP, Node does not automatically load lots of server features for you. You explicitly import what you need.
−
* <code>http.createServer(function (req, res) { ... }).listen(3456);</code> uses the HTTP server API to listen for HTTP connections on port 3456. The anonymous callback function will be called whenever a connection is made.
+
* <code>http.createServer((req, res) =&gt; { ... }).listen(3456);</code> listens for HTTP connections on port 3456. The callback function runs whenever a connection is made.
−
* <code>res.writeHead(200, { 'Content-Type': 'text/plain' });</code> specifies the Content-Type header to be "text/plain".
+
* <code>res.writeHead(200, { "Content-Type": "text/plain" });</code> sets the HTTP response code and headers.
−
* <code>res.end('Hello World\n');</code> writes "Hello World" to the response.
+
* <code>res.end("Hello World\n");</code> writes the response body and ends the response.
  
−
Note the use of non-blocking I/O, even in this one little example.  ''Whenever a connection is made, the callback function will be called and passed parameters associated with that connection.''
+
Notice the use of non-blocking behavior even in this simple example: ''when a connection is made'', the callback runs with the parameters associated with that connection.
  
 
== A Static File Server Using Node.js ==
 
== A Static File Server Using Node.js ==
  
−
Node.js does not automatically serve up static files in the way that an Apache/PHP does. We need to either use an extension that handles this for us, like Express, or we need to write our own code to make a simple static file server.  The latter option is really not all that difficult, and it gives good insight into how a static file server works.
+
Node.js does not automatically serve up static files in the way that Apache/PHP does. We need to either:
 +
 
 +
* use a framework like Express (common in industry), or
 +
* write a simple static file server ourselves (great for understanding how servers work)
 +
 
 +
We'll do the second option here.
  
 
=== Installing Packages ===
 
=== Installing Packages ===
  
−
In order for the example code below to work, you need to install the ''mime'' package. To do this, '''cd to the working directory where you will save your Node.js JavaScript file''' and run:
+
In order for the example code below to work, you need the '''mime''' package (it helps map filenames to Content-Type headers). The `mime` package is actively maintained.
  
−
<source lang="bash">$ npm install mime --save</source>
+
In the directory where you will save your Node.js file:
 +
 
 +
<source lang="bash">
 +
$ npm init -y
 +
$ npm install mime
 +
</source>
  
−
''npm'' is the package manager for Node.js. Unlike all of the package managers we've used up to this point, ''npm'' installs dependencies locally, ''not'' globally.  This means that you can have two different projects using different versions of the same package.  ''npm'' will create a ''node_modules'' directory in the CWD and will install the packages into it.
+
''npm'' is the package manager for Node.js. It installs dependencies locally (inside `node_modules`) by default, which is a good thing: it lets different projects use different versions of the same dependency.
  
−
'''Note:''' When using npm to install local dependencies, '''''do not use sudo'''''. It will screw up your file permissions.
+
'''Note:''' When using npm to install local dependencies, '''do not use sudo'''. It can break file permissions in your project directory.
  
−
=== Static Fileserver Code ===
+
=== Static Fileserver Code (Modern ESM + async/await) ===
  
−
The following code will make an HTTP server on port 3456 that serves up all static files in <STATIC DIRECTORY NAME> relative to app.js.
+
The following code makes an HTTP server on port 3456 that serves all static files inside <STATIC DIRECTORY NAME> relative to app.js.
  
−
'''Make an attempt to understand this code.''' It's important that you are comfortable with the non-blocking nature, as well as how we handle file I/O.  
+
'''Make an attempt to understand this code.''' It's important that you are comfortable with the non-blocking nature, as well as how we handle file I/O.
  
 
<source lang="javascript">
 
<source lang="javascript">
−
// Require the functionality we need to use:
+
import http from "node:http";
−
var http = require('http'),
+
import path from "node:path";
−
url = require('url'),
+
import { readFile } from "node:fs/promises";
−
mime = require('mime'),
+
import { existsSync } from "node:fs";
−
path = require('path'),
+
import mime from "mime";
−
fs = require('fs');
+
import { fileURLToPath } from "node:url";
 +
 
 +
const __filename = fileURLToPath(import.meta.url);
 +
const __dirname = path.dirname(__filename);
 +
 
 +
const STATIC_DIR = "<STATIC DIRECTORY NAME>";
 +
 
 +
const app = http.createServer(async (req, resp) => {
 +
try {
 +
// Parse the path the browser requested
 +
const urlPath = new URL(req.url, "http://localhost").pathname;
 +
const filename = path.join(__dirname, STATIC_DIR, decodeURIComponent(urlPath));
  
−
// Make a simple fileserver for all of our static content.
+
// If file doesn't exist, return 404
−
// Everything underneath <STATIC DIRECTORY NAME> will be served.
+
if (!existsSync(filename)) {
−
var app = http.createServer(function(req, resp){
+
resp.writeHead(404, { "Content-Type": "text/plain" });
−
var filename = path.join(__dirname, "<STATIC DIRECTORY NAME>", url.parse(req.url).pathname);
+
resp.end("Requested file not found: " + filename);
−
(fs.exists || path.exists)(filename, function(exists){
 
−
if (exists) {
 
−
fs.readFile(filename, function(err, data){
 
−
if (err) {
 
−
// File exists but is not readable (permissions issue?)
 
−
resp.writeHead(500, {
 
−
"Content-Type": "text/plain"
 
−
});
 
−
resp.write("Internal server error: could not read file");
 
−
resp.end();
 
−
return;
 
−
}
 
−
 
−
// File exists and is readable
 
−
var mimetype = mime.getType(filename);
 
−
resp.writeHead(200, {
 
−
"Content-Type": mimetype
 
−
});
 
−
resp.write(data);
 
−
resp.end();
 
−
return;
 
−
});
 
−
}else{
 
−
// File does not exist
 
−
resp.writeHead(404, {
 
−
"Content-Type": "text/plain"
 
−
});
 
−
resp.write("Requested file not found: "+filename);
 
−
resp.end();
 
 
return;
 
return;
 
}
 
}
−
});
+
 
 +
// Read the file (non-blocking I/O via promises)
 +
const data = await readFile(filename);
 +
 
 +
// Determine content type
 +
const mimetype = mime.getType(filename) || "application/octet-stream";
 +
resp.writeHead(200, { "Content-Type": mimetype });
 +
resp.end(data);
 +
} catch (err) {
 +
// File exists but isn't readable or some other error occurred
 +
resp.writeHead(500, { "Content-Type": "text/plain" });
 +
resp.end("Internal server error: could not read file");
 +
}
 
});
 
});
 +
 
app.listen(3456);
 
app.listen(3456);
 
</source>
 
</source>
 +
 +
==== A few important notes about the modern version ====
 +
 +
* We use <code>node:fs/promises</code> so we can write file I/O using async/await.
 +
* In ES Modules, <code>__dirname</code> is not automatically defined, so we compute it using <code>import.meta.url</code>.
 +
* This server is intentionally simple for instructional purposes.
 +
 +
'''Security note:''' A production static server must protect against directory traversal attacks (paths like <code>../</code>). For this course example, we focus on learning the mechanics first.
  
 
=== Port 3456 ===
 
=== Port 3456 ===
  
−
We are running our server on port 3456 as opposed to the default web server port, which of course is 80. We therefore need to open our Node.js application like this:
+
We are running our server on port 3456 instead of port 80. We therefore need to open our Node.js application like this:
  
 
http://ec2-xx-xx-xx-xx.compute-1.amazonaws.com:3456/path.extension
 
http://ec2-xx-xx-xx-xx.compute-1.amazonaws.com:3456/path.extension
  
−
However, this won't work on the first time. ''Why?'' (Try to figure this out on your own before reading the next paragraph.)
+
However, this won't work the first time. ''Why?''
  
−
We need to open port 3456 on our Amazon EC2 instance. Remember when you opened port 80 way back in Module 2? We need to do the same sort of procedure again now.
+
We need to open port 3456 on our Amazon EC2 instance. Remember when you opened port 80 back in Module 2? We need to do the same procedure again now.
  
 
# Log into your EC2 management console
 
# Log into your EC2 management console
Line 175: Line 237:
 
== Node.js Security ==
 
== Node.js Security ==
  
−
Just because we're writing in JavaScript doesn't mean that we can forget about the security practices you've been learning in the last three months of CSE 330. XSS is still a problem, as is the use of ''eval'', CSRF attacks, and so on.
+
Just because we're writing in JavaScript doesn't mean we can forget about the security practices you've been learning in CSE 330. XSS is still a problem, as is the use of ''eval'', CSRF attacks, and so on.
−
 
 
−
The two links below have good information about security vulnerabilities specific to Node.js that you should think about when crafting your application.
 
−
 
 
−
# Book from PragProg ($24, 2015): https://pragprog.com/book/kdnodesec/secure-your-node-js-web-application
 
−
# Slideshow from a Switzerland Conference on Application Security (2012): http://www.slideshare.net/ASF-WS/asfws-2012-nodejs-security-old-vulnerabilities-in-new-dresses-par-sven-vetsch
 
  
 
== Node.js Conventions ==
 
== Node.js Conventions ==
  
−
There ought to be a standard way to save metadata about your app, right? Well, ''npm'' introduces a straightforward way to do this in the form of a ''package.json'' file.
+
There ought to be a standard way to save metadata about your app, right? Node projects use a <code>package.json</code> file for this.
  
−
To make your ''npm'' file with a step-by-step prompt, run in your project root directory:
+
To create one quickly:
  
 
<source lang="bash">
 
<source lang="bash">
−
$ npm init
+
$ npm init -y
 
</source>
 
</source>
  
−
One nifty feature of ''package.json'' is the way it automatically handles dependencies. For example, you can put the following lines in your ''package.json'':
+
One nice feature of <code>package.json</code> is that it automatically handles dependencies. For example:
  
 
<source lang="javascript">
 
<source lang="javascript">
−
  "dependencies": {
+
"dependencies": {
−
    "mime": "~2.4.6",
+
  "mime": "^4.1.0",
−
    "socket.io": "~1.0.0"
+
  "socket.io": "^4.8.0"
−
  },
+
}
 
</source>
 
</source>
  
−
Then, back on the command line, when you run
+
Then when you run:
  
 
<source lang="bash">
 
<source lang="bash">
Line 207: Line 264:
 
</source>
 
</source>
  
−
it automatically installs the dependencies for you!  No more headaches of trying to figure out which modules you need to install; all that information is saved for you in your code base.
+
npm installs the dependencies for you.
  
−
'''Tip:''' Once you set up your ''package.json'' file, you can automatically add a dependency to it by using the ''--save'' argument on ''npm install'': <code>npm install --save socket.io</code>
+
'''Tip:''' Modern npm automatically saves dependencies when you run <code>npm install package-name</code>. The older <code>--save</code> flag is no longer necessary.
  
−
[[Category:Module 7]]
+
[[Category:Module 5]]

Latest revision as of 22:38, 21 October 2026

AJAX is a useful technology for web applications that need to pull periodic updates from your server. But what do you do when you have thousands of clients all requesting data that changes in real-time? A PHP+AJAX approach could struggle without a lot of effort on your part.

It is situations like these that Node.js comes to the rescue.

Node.js is a JavaScript runtime (built on Chrome's V8 engine) that lets you run JavaScript outside the browser. Frameworks like Express run on top of Node.js.

In general, JavaScript frameworks are trendy; it can be hard to keep up with the new cool thing. In fact, there's a joke website https://dayssincelastjavascriptframework.com/ that "tracks" how long it's been since a new framework has come out.

Non-Blocking I/O

You dabbled in the concept of non-blocking I/O when you implemented AJAX in the previous JavaScript module. Node.js uses this execution model at its core.

A simple way to think about Node:

  • Node is designed to handle lots of connections efficiently.
  • Instead of waiting for slow operations (network, file system, database) to finish, Node starts the work and continues doing other things.
  • When the slow operation finishes, Node runs some code to handle the result.

Historically, the core idea was the use of anonymous functions as callbacks for processing the results of intensive operations. Consider the pseudo-code:

// Blocking I/O Pseudo-Code

var data = database.query("select * from news");
print data[0].title;

The above pseudo-code queries a database, saves the result of the query in a variable named data, and then prints out the first title in data. The problem arises when multiple clients all want to access the server at the same time.

In a blocking I/O model, a client needs to wait until all other clients are finished before it can start its process. With intensive operations and thousands of clients, this can easily crash your server (or force you to provision much more hardware than you want).

The solution is to use non-blocking I/O. Consider this revised pseudo-code:

// Non-Blocking I/O Pseudo-Code

database.query("select * from news", function(data){
	print data[0].title;
});

The revised pseudo-code achieves the same end result as the first version. However, it allows other clients to perform tasks while this process is waiting for the database to be queried!

The code occurs in two parts:

  1. Start the query.
  2. When the query is complete, run the callback function.

In Node.js, callback functions are usually passed one or more parameters.

Modern Node style: Promises and async/await

Callbacks are still important to understand (they explain why Node "works"), but modern Node code often uses Promises and async/await because they make programs easier to read and debug.

Conceptually:

// Promise + async/await pseudo-code

const data = await database.query("select * from news");
console.log(data[0].title);

This is still non-blocking behind the scenes—it's just nicer syntax for “start work now, continue later when it finishes.”

One thread? Concurrency?

You will often hear that Node “runs in one thread.” The key point for this course is:

  • You can handle many concurrent connections without making a thread per client.
  • The server remains responsive because it avoids blocking on slow I/O.

(There are deeper details in the real Node event loop model, but the above understanding will serve you well for CSE 3300.)

Installing Node.js

We will install Node using nvm (Node Version Manager). This makes it easy to switch Node versions and consistently use the current LTS version.

Run the following commands:

$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash
$ source ~/.bashrc
$ nvm install --lts
$ node -v
$ npm -v

Note: As of October 2026, Node 24 is the Active LTS line. Using `nvm install --lts` means you do not have to hardcode a specific major version in the instructions.

Your First Node.js Application: Hello World

Let's try the classic HTTP server example.

Modern version (ES Modules)

Create a file named `hello.js`:

import http from "node:http";

http.createServer((req, res) => {
	res.writeHead(200, { "Content-Type": "text/plain" });
	res.end("Hello World\n");
}).listen(3456);

console.log("Server running at http://localhost:3456/");

To run it:

$ node hello.js

Note: If you're running the code from your EC2 instance, you will need to open port 3456.

Enabling ES Modules (important!)

Modern Node supports both the older CommonJS module system (require) and the newer JavaScript standard ES Modules (import/export). ES Modules are the direction modern JavaScript has moved (and they match browser JavaScript).

To tell Node to treat `.js` files as ES Modules, add this to `package.json` in your project directory:

{
  "type": "module"
}

If you do not do this, Node may treat `.js` files as CommonJS by default, and `import ...` may fail.

Walkthrough of the Hello World example

  • import http from "node:http"; enables us to use Node's built-in HTTP server library. Unlike PHP, Node does not automatically load lots of server features for you. You explicitly import what you need.
  • http.createServer((req, res) => { ... }).listen(3456); listens for HTTP connections on port 3456. The callback function runs whenever a connection is made.
  • res.writeHead(200, { "Content-Type": "text/plain" }); sets the HTTP response code and headers.
  • res.end("Hello World\n"); writes the response body and ends the response.

Notice the use of non-blocking behavior even in this simple example: when a connection is made, the callback runs with the parameters associated with that connection.

A Static File Server Using Node.js

Node.js does not automatically serve up static files in the way that Apache/PHP does. We need to either:

  • use a framework like Express (common in industry), or
  • write a simple static file server ourselves (great for understanding how servers work)

We'll do the second option here.

Installing Packages

In order for the example code below to work, you need the mime package (it helps map filenames to Content-Type headers). The `mime` package is actively maintained.

In the directory where you will save your Node.js file:

$ npm init -y
$ npm install mime

npm is the package manager for Node.js. It installs dependencies locally (inside `node_modules`) by default, which is a good thing: it lets different projects use different versions of the same dependency.

Note: When using npm to install local dependencies, do not use sudo. It can break file permissions in your project directory.

Static Fileserver Code (Modern ESM + async/await)

The following code makes an HTTP server on port 3456 that serves all static files inside <STATIC DIRECTORY NAME> relative to app.js.

Make an attempt to understand this code. It's important that you are comfortable with the non-blocking nature, as well as how we handle file I/O.

import http from "node:http";
import path from "node:path";
import { readFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import mime from "mime";
import { fileURLToPath } from "node:url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const STATIC_DIR = "<STATIC DIRECTORY NAME>";

const app = http.createServer(async (req, resp) => {
	try {
		// Parse the path the browser requested
		const urlPath = new URL(req.url, "http://localhost").pathname;
		const filename = path.join(__dirname, STATIC_DIR, decodeURIComponent(urlPath));

		// If file doesn't exist, return 404
		if (!existsSync(filename)) {
			resp.writeHead(404, { "Content-Type": "text/plain" });
			resp.end("Requested file not found: " + filename);
			return;
		}

		// Read the file (non-blocking I/O via promises)
		const data = await readFile(filename);

		// Determine content type
		const mimetype = mime.getType(filename) || "application/octet-stream";
		resp.writeHead(200, { "Content-Type": mimetype });
		resp.end(data);
	} catch (err) {
		// File exists but isn't readable or some other error occurred
		resp.writeHead(500, { "Content-Type": "text/plain" });
		resp.end("Internal server error: could not read file");
	}
});

app.listen(3456);

A few important notes about the modern version

  • We use node:fs/promises so we can write file I/O using async/await.
  • In ES Modules, __dirname is not automatically defined, so we compute it using import.meta.url.
  • This server is intentionally simple for instructional purposes.

Security note: A production static server must protect against directory traversal attacks (paths like ../). For this course example, we focus on learning the mechanics first.

Port 3456

We are running our server on port 3456 instead of port 80. We therefore need to open our Node.js application like this:

http://ec2-xx-xx-xx-xx.compute-1.amazonaws.com:3456/path.extension

However, this won't work the first time. Why?

We need to open port 3456 on our Amazon EC2 instance. Remember when you opened port 80 back in Module 2? We need to do the same procedure again now.

  1. Log into your EC2 management console
  2. Go to "Security Groups"
  3. Select your security group from Module 2
  4. Under "Inbound", create a custom TCP rule for port 3456, and apply your changes.

Node.js Security

Just because we're writing in JavaScript doesn't mean we can forget about the security practices you've been learning in CSE 330. XSS is still a problem, as is the use of eval, CSRF attacks, and so on.

Node.js Conventions

There ought to be a standard way to save metadata about your app, right? Node projects use a package.json file for this.

To create one quickly:

$ npm init -y

One nice feature of package.json is that it automatically handles dependencies. For example:

"dependencies": {
  "mime": "^4.1.0",
  "socket.io": "^4.8.0"
}

Then when you run:

$ npm install

npm installs the dependencies for you.

Tip: Modern npm automatically saves dependencies when you run npm install package-name. The older --save flag is no longer necessary.