Difference between revisions of "Node.JS"

From CSE330 Wiki
Jump to navigationJump to search
 
(39 intermediate revisions by 8 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 would choke. It is situations like these that Node.JS comes to the rescue.
+
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 ==
 
== 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.
  
−
The core idea is the use of '''''anonymous functions''''' as ''callbacks'' for processing the results of intensive operations. Consider the pseudo-code:
+
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:
  
 
<source lang="javascript">
 
<source lang="javascript">
Line 14: 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 26: 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.
 +
 
 +
(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:
 +
 
 +
<source lang="bash">
 +
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash
 +
$ source ~/.bashrc
 +
$ nvm install --lts
 +
$ node -v
 +
$ npm -v
 +
</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";
 +
 
 +
http.createServer((req, res) => {
 +
res.writeHead(200, { "Content-Type": "text/plain" });
 +
res.end("Hello World\n");
 +
}).listen(3456);
  
−
Better yet, with non-blocking I/O, you have no concurrency issues, because everything runs in just one thread.  Nifty!
+
console.log("Server running at http://localhost:3456/");
 +
</source>
  
−
== Installing Node.JS ==
+
To run it:
  
−
You can get Node.JS from yum or apt-get, but the versions are going to be out dated.  Remember that Node.JS is a rapidly changing language, so a version of Node from last year is going to be drastically different than a version of Node today.
+
<source lang="bash">
 +
$ node hello.js
 +
</source>
  
−
The unofficial instructions for installing Node.JS can be found here: https://gist.github.com/isaacs/579814
+
'''Note:''' If you're running the code from your EC2 instance, you will need to open port 3456.
  
−
There are several choices, but my favorite is probably ''use-nave-no-shell.sh''.
+
=== 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">
−
var http = require('http');
+
{
−
http.createServer(function (req, res) {
+
  "type": "module"
−
res.writeHead(200, {
+
}
−
'Content-Type': 'text/plain'
 
−
});
 
−
res.end('Hello World\n');
 
−
}).listen(1337);
 
−
console.log('Server running at http://localhost:1337/');
 
 
</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 1337!  (Press ^C to stop the application.)  '''Note:''' If you're running the code from your EC2 instance, you will need to open port 1337.
+
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(1337);</code> uses the HTTP server API to listen for HTTP connections on port 1337. 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 Fileserver 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 fileserver.  The latter option is really not all that difficult, and it gives good insight into how a static fileserver 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</source>
+
In the directory where you will save your Node.js file:
  
−
''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.
+
<source lang="bash">
 +
$ npm init -y
 +
$ npm install mime
 +
</source>
 +
 
 +
''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, '''''please do not use sudo'''''.
+
'''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.  There may be quiz questions about this code.
+
'''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";
−
path = require('path'),
+
import { existsSync } from "node:fs";
−
mime = require('mime'),
+
import mime from "mime";
−
path = require('path'),
+
import { fileURLToPath } from "node:url";
−
fs = require('fs');
+
 
 +
const __filename = fileURLToPath(import.meta.url);
 +
const __dirname = path.dirname(__filename);
  
−
// Make a simple fileserver for all of our static content.
+
const STATIC_DIR = "<STATIC DIRECTORY NAME>";
−
// Everything underneath <STATIC DIRECTORY NAME> will be served.
+
 
−
var app = http.createServer(function(req, resp){
+
const app = http.createServer(async (req, resp) => {
−
var filename = path.join(__dirname, "<STATIC DIRECTORY NAME>", url.parse(req.url).pathname);
+
try {
−
(fs.exists || path.exists)(filename, function(exists){
+
// Parse the path the browser requested
−
if (exists) {
+
const urlPath = new URL(req.url, "http://localhost").pathname;
−
fs.readFile(filename, function(err, data){
+
const filename = path.join(__dirname, STATIC_DIR, decodeURIComponent(urlPath));
−
if (err) {
+
 
−
// File exists but is not readable (permissions issue?)
+
// If file doesn't exist, return 404
−
resp.writeHead(500, {
+
if (!existsSync(filename)) {
−
"Content-Type": "text/plain"
+
resp.writeHead(404, { "Content-Type": "text/plain" });
−
});
+
resp.end("Requested file not found: " + filename);
−
resp.write("Internal server error: could not read file");
 
−
resp.end();
 
−
return;
 
−
}
 
−
 
−
// File exists and is readable
 
−
var mimetype = mime.lookup(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 148: Line 235:
 
# Under "Inbound", create a custom TCP rule for port 3456, and apply your changes.
 
# Under "Inbound", create a custom TCP rule for port 3456, and apply your changes.
  
−
== 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.
  
−
Here is a good slideshow introducing Node.JS as well as some security vulnerabilities to look out for: 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? Node projects use a <code>package.json</code> file for this.
  
−
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.
+
To create one quickly:
−
 
 
−
To make your ''npm'' file with a step-by-step prompt, run in your project root directory:
 
  
 
<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": "~1.2.11",
+
  "mime": "^4.1.0",
−
    "socket.io": "~0.9.16",
+
  "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 179: 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:''' 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.