Module5-NodeJS-TEST

From CSE330 Wiki
Jump to navigationJump to search

Node.js is a JavaScript runtime built on Chrome's V8 engine that allows JavaScript to run on the server. Frameworks such as Express run on top of Node.js.

Node.js is especially useful for real-time, high-concurrency applications due to its non-blocking, event-driven execution model.

Non-Blocking I/O

Node.js uses a non-blocking execution model. Instead of waiting for operations (like database queries or file reads) to complete, Node continues executing other work and runs a callback or Promise when the operation finishes.

Blocking example (conceptual):

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 must wait until all other clients finish before it can start its process. With intensive operations and thousands of clients, this can easily crash your server!

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

Non-blocking example:

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: 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 (often an error parameter and a result parameter).

Better yet, with non-blocking I/O, you avoid many of the concurrency headaches common in multi-threaded server environments, because JavaScript code in Node runs in a single main thread. Nifty!

Important clarification: "Single-threaded" does not mean "no concurrency." Node can handle many concurrent connections. The difference is that it avoids running multiple threads of JavaScript at the same time, which reduces race conditions and locking complexity. The concurrency happens in the underlying event loop and operating system, not by spawning a new JavaScript thread per request.

Modern Syntax: Promises and async/await

Historically, Node relied heavily on callback functions, like the example above. While this model still works, large programs with many nested callbacks can become difficult to read and maintain (sometimes called "callback hell").

Modern Node commonly uses Promises and the async/await syntax built on top of them. Conceptually, the behavior is identical: the operation begins, Node continues handling other work, and when the operation finishes, your code resumes.

For example:

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

Behind the scenes, this is still non-blocking I/O. The `await` keyword does not freeze the entire server; it simply pauses that particular function until the Promise resolves, while the event loop continues processing other incoming requests.

So:

  • Callbacks explain how Node originally structured asynchronous work.
  • Promises formalize asynchronous behavior.
  • async/await makes asynchronous code look more like synchronous code — but it remains non-blocking underneath.

Understanding the callback model helps you understand how Node works internally. Understanding async/await helps you write modern, readable Node applications.

Installing Node.js

We use nvm (Node Version Manager) to install Node:

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

Your First Node.js Application

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/");

Run:

$ node hello.js

Important: Modern Node uses ES Modules. Add this to package.json:

{
  "type": "module"
}

Installing Packages with npm

Install dependencies locally:

$ npm install mime

npm automatically records dependencies in package.json.

Static File Server

The following server serves static files from a directory:

import http from "node:http";
import { readFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import path from "node:path";
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 {
		const urlPath = new URL(req.url, "http://localhost").pathname;
		const filename = path.join(__dirname, STATIC_DIR, decodeURIComponent(urlPath));

		if (!existsSync(filename)) {
			resp.writeHead(404, { "Content-Type": "text/plain" });
			resp.end("File not found");
			return;
		}

		const data = await readFile(filename);
		const mimetype = mime.getType(filename) || "application/octet-stream";

		resp.writeHead(200, { "Content-Type": mimetype });
		resp.end(data);
	} catch {
		resp.writeHead(500);
		resp.end("Server error");
	}
});

app.listen(3456);

Security Note: Production servers must validate file paths to prevent directory traversal attacks.

Opening Port 3456 on EC2

Follow the same process used previously to open port 80, but create a rule for TCP port 3456.

Node.js Security

Common risks include:

  • XSS
  • CSRF
  • Injection attacks
  • Unsafe use of eval()
  • Directory traversal

Always validate inputs and sanitize outputs.

Node.js Conventions

Initialize a project:

$ npm init -y

Install dependencies:

$ npm install

package.json stores metadata and dependencies.