Difference between revisions of "Node.JS"
| (12 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. | + | 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 | + | 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. | + | 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: | |
<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 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''. | + | ''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. | + | 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 == | == Installing Node.js == | ||
| − | Run the following commands | + | 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. | + | $ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash |
| − | |||
$ source ~/.bashrc | $ source ~/.bashrc | ||
| − | - | + | $ nvm install --lts |
| − | $ | + | $ 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"; | ||
| − | + | 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/"); | |
| + | </source> | ||
| − | + | To run it: | |
<source lang="bash"> | <source lang="bash"> | ||
| − | $ | + | $ node hello.js |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
</source> | </source> | ||
| − | + | '''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: | |
<source lang="javascript"> | <source lang="javascript"> | ||
| − | + | { | |
| − | + | "type": "module" | |
| − | + | } | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
</source> | </source> | ||
| − | + | 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 === | |
| − | * <code> | + | * <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( | + | * <code>http.createServer((req, res) => { ... }).listen(3456);</code> listens for HTTP connections on port 3456. The callback function runs whenever a connection is made. |
| − | * <code>res.writeHead(200, { | + | * <code>res.writeHead(200, { "Content-Type": "text/plain" });</code> sets the HTTP response code and headers. |
| − | * <code>res.end( | + | * <code>res.end("Hello World\n");</code> 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 == | == A Static File Server Using Node.js == | ||
| − | Node.js does not automatically serve up static files in the way that | + | 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 | + | 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 | + | 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. | + | ''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, | + | '''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 | + | 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.''' | + | '''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"> | ||
| − | / | + | 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"; | |
| − | path = | + | 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); | |
| − | |||
| − | if ( | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | resp.writeHead(404, { | ||
| − | |||
| − | |||
| − | resp. | ||
| − | |||
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 | + | 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 | + | However, this won't work the first time. ''Why?'' |
| − | We need to open port 3456 on our Amazon EC2 instance. | + | 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 176: | Line 237: | ||
== Node.js Security == | == Node.js Security == | ||
| − | Just because we're writing in JavaScript doesn't mean | + | 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 == | == Node.js Conventions == | ||
| − | There ought to be a standard way to save metadata about your app, right? | + | 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 | + | To create one quickly: |
<source lang="bash"> | <source lang="bash"> | ||
| − | $ npm init | + | $ npm init -y |
</source> | </source> | ||
| − | One | + | One nice feature of <code>package.json</code> is that it automatically handles dependencies. For example: |
<source lang="javascript"> | <source lang="javascript"> | ||
| − | + | "dependencies": { | |
| − | + | "mime": "^4.1.0", | |
| − | + | "socket.io": "^4.8.0" | |
| − | + | } | |
</source> | </source> | ||
| − | Then | + | Then when you run: |
<source lang="bash"> | <source lang="bash"> | ||
| Line 208: | Line 264: | ||
</source> | </source> | ||
| − | + | npm installs the dependencies for you. | |
| − | '''Tip:''' | + | '''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 | + | [[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.
Contents
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:
- 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:
// 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/promisesso we can write file I/O using async/await. - In ES Modules,
__dirnameis not automatically defined, so we compute it usingimport.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.
- Log into your EC2 management console
- Go to "Security Groups"
- Select your security group from Module 2
- 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.