Node.JS

From CSE330 Wiki
Jump to navigationJump to search

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. :contentReference[oaicite:3]{index=3}

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. :contentReference[oaicite:4]{index=4}

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 February 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. :contentReference[oaicite:6]{index=6}

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). :contentReference[oaicite:7]{index=7}

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

{
  "type": "module"
}
contentReference[oaicite:8]{index=8}

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. :contentReference[oaicite:9]{index=9}

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
contentReference[oaicite:10]{index=10}

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.