Code walkthroughs from the CS 35L Lecture 5 — Client Server & Node.js. Covers JavaScript basics, JSON, blocking vs. non-blocking I/O, the built-in http module, and Express.js routing.
JavaScript is an interpreted, dynamically typed programming language — similar to Python. It can run in the browser, but with Node.js it can also run on the server.
// A function that takes two arguments
function add(num1, num2) {
let sum = num1 + num2;
return sum; // Returns the result
}
let result = add(5, 7); // result is now 12
console.log(result); // Output: 12
function declares a named function (similar to def in Python)let declares a mutable variableconsole.log() is the Node.js equivalent of Python’s print()Run the code as-is. Then change the arguments to add(10, 32) and predict the output before running again.
// A function that takes two arguments
function add(num1, num2) {
let sum = num1 + num2;
return sum; // Returns the result
}
let result = add(5, 7); // result is now 12
console.log(result); // Output: 12
1. A teammate writes this code:
function multiply(a, b) {
let result = a * b;
let result = a + b;
return result;
}
let does not allow redeclaring the same variable in the same scope — the parser throws SyntaxError: Identifier 'result' has already been declared before any code runs. This is one advantage let has over var: var would silently allow the redeclaration, hiding the bug.
2. A library module declares let MAX_CONNECTIONS = 100 at the top. The value never changes anywhere in the 500-line file. A teammate flags this in code review and asks you to change it to const. Is this a valid concern?
const makes intent explicit: any reader immediately knows the value will never change, reducing cognitive load. It also makes a future accidental reassignment a compile-time error rather than a silent bug. This is a meaningful code-quality flag, not merely a style preference.
JSON is a human-readable, lightweight data-interchange format used to transfer data between a server and a web page, or between applications.
It is the standard format for REST API responses. In JavaScript, objects look almost identical to JSON:
const person = {
"address-dict": {
"city": "New York",
"residential": true,
"postal_code": 10023
},
"phone_numbers": [
{ "type": "home", "number": "212 555-1234" },
{ "type": "office", "number": "646 555-4567" }
],
"children": ["Catherine", "Thomas", "Trevor"]
};
| Method | Purpose |
|---|---|
JSON.stringify(obj) |
Convert a JS object → JSON string |
JSON.parse(str) |
Parse a JSON string → JS object |
Run the code to see JSON serialisation and parsing in action. Notice how JSON.stringify(person, null, 2) pretty-prints the object with 2-space indentation.
const person = {
"address-dict": {
"city": "New York",
"residential": true,
"postal_code": 10023
},
"phone_numbers": [
{ "type": "home", "number": "212 555-1234" },
{ "type": "office", "number": "646 555-4567" }
],
"children": ["Catherine", "Thomas", "Trevor"]
};
// Serialize JS object → JSON string
const jsonString = JSON.stringify(person, null, 2);
console.log("=== JSON.stringify ===");
console.log(jsonString);
// Parse JSON string back into a JS object
const parsed = JSON.parse(jsonString);
console.log("\n=== Parsed city ===");
console.log(parsed["address-dict"].city); // New York
1. A developer writes the following route handler:
const user = { name: 'Alice', password: 'secret123', id: 42 };
res.send(JSON.stringify(user));
Serialising the full object blindly sends every field — including password — to whoever made the request. The fix is to explicitly select safe fields: res.json({ name: user.name, id: user.id }). Accidentally leaking sensitive data through JSON serialisation is one of the most common REST API security mistakes.
2. Which of the following strings is not valid JSON?
JSON requires all keys to be double-quoted strings. { name: "Alice" } is valid JavaScript object-literal syntax but invalid JSON — JSON.parse('{ name: "Alice" }') throws a SyntaxError. This distinction matters whenever you receive data from an external API and try to parse it.
Node.js is a JavaScript runtime for asynchronous events. It is single-threaded, so blocking the main thread means no other requests can be handled.
const fs = require('fs');
const data = fs.readFileSync('/file.md');
// blocks here until file is read
readFileSync stops execution until the file is fully read.
Bad for servers — every other request waits!
fs.readFile('/file.md', (err, data) => {
if (err) { /* handle error */ }
// use data here
});
// following code is executed instantly
The callback (err, data) => { ... } is an asynchronous callback: it executes after the data has been completely stored in data. Meanwhile Node.js keeps processing other events.
For longer handlers, extract the callback into a named function — this improves readability and enables reuse:
function handleMarkdownRead(err, data) {
if (err) { /* handle error */ }
// use data
}
fs.readFile('/file.md', handleMarkdownRead);
// following code is executed instantly
Node.js philosophy: use non-blocking code for all non-instant tasks to maintain high performance and scalability.
Run the code. Notice that "After readFile call" is printed before the file contents — that is the non-blocking nature of Node.js in action.
Hello from the file system!
This text was read asynchronously.
const fs = require('fs');
// --- Blocking ---
console.log("=== Blocking readFileSync ===");
const dataSync = fs.readFileSync('message.txt', 'utf8');
console.log(dataSync);
console.log("(This line runs AFTER the file is read)\n");
// --- Non-Blocking (inline callback) ---
console.log("=== Non-Blocking readFile ===");
fs.readFile('message.txt', 'utf8', (err, data) => {
if (err) {
console.error("Error:", err.message);
return;
}
console.log("Callback fired:", data);
});
console.log("After readFile call — runs BEFORE callback fires\n");
// --- Named Callback ---
function handleFileRead(err, data) {
if (err) {
console.error("Error:", err.message);
return;
}
console.log("Named callback fired:", data);
}
console.log("=== Non-Blocking with named callback ===");
fs.readFile('message.txt', 'utf8', handleFileRead);
console.log("After named callback registration");
1. A Node.js HTTP server handles requests using a handler that calls fs.readFileSync('config.json'), which takes 40 ms. The server receives 100 concurrent requests per second. What is the effective throughput?
Node.js is single-threaded. A 40 ms readFileSync freezes the entire event loop for 40 ms — no other requests can be processed during that window. Maximum throughput is 1000 ms ÷ 40 ms = 25 requests/second regardless of how many arrive. The remaining 75 queue up and time out. This is precisely why readFile (non-blocking) exists.
2. A student writes a small CLI tool that reads a configuration file once at startup and then exits. They use fs.readFileSync. A teammate says this is always wrong and should always be fs.readFile. Who is right?
The danger of readFileSync is blocking the event loop and starving concurrent requests. In a one-shot CLI script there is no event loop running and no concurrent requests — the program starts, reads the file, acts, and exits. readFileSync is simpler and completely appropriate here. Node.js documentation itself recommends synchronous APIs for startup configuration in non-server contexts.
Node.js ships with a built-in http module. You can create a server without any third-party packages:
const http = require('http');
const PORT = 8080;
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello, World!\n');
});
server.listen(PORT, 'localhost', () => {
console.log(`Server running at http://localhost:${PORT}/`);
});
| Line | What it does |
|---|---|
http.createServer((req, res) => { ... }) |
Creates an HTTP server; the callback fires on every incoming request |
res.writeHead(200, { 'Content-Type': 'text/plain' }) |
Sets HTTP status 200 (OK) and the Content-Type header |
res.end('Hello, World!\n') |
Writes the response body and closes the connection |
server.listen(PORT, 'localhost', callback) |
Starts listening on port 8080 |
The callback (req, res) => { ... } passed to createServer is called on every HTTP request — this is Node’s callback pattern applied to networking.
Click ▶ Run to start the server, then use the HTTP Client on the right to send a GET request to http://localhost:8080/ and observe the response.
const http = require('http');
const PORT = 8080;
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello, World!\n');
});
server.listen(PORT, 'localhost', () => {
console.log(`Server running at http://localhost:${PORT}/`);
});
1. The current server responds with Hello, World! to every request regardless of URL or method. What change would you make inside the http.createServer callback to return a different response for GET /status versus all other paths?
req.url holds the path of every incoming request (e.g. '/status'). The single callback receives all requests, so you branch on req.url (and req.method) to route manually. This manual branching is exactly the boilerplate that Express eliminates with app.get('/status', ...).
2. You are starting a new project: a REST API with 12 endpoints, JSON request bodies, and authentication middleware. Should you use the built-in http module or Express?
For a multi-endpoint API, manually parsing URLs, matching HTTP methods, and reading request bodies in raw http adds hundreds of lines of repetitive, error-prone code. Express handles all of this declaratively. The performance gap between raw http and Express is negligible at realistic request rates. The http module shines for very simple use cases or when fine-grained control is needed — not for a 12-endpoint authenticated API.
The built-in http module is powerful but low-level. Express is the most popular Node.js web framework. It makes routing simple:
"When someone visits THIS URL with THIS method, call THIS function."
const express = require('express');
const app = express();
const port = 8080;
// GET /users/:userId — route parameter
app.get('/users/:userId', (req, res) => {
res.send(`GET request to user ${req.params.userId}`);
});
// POST /
app.post('/', (req, res) => {
res.send('POST request to the homepage');
});
// GET /about
app.get('/about', (req, res) => {
res.send('About page');
});
// Catch-all — must be LAST
app.all('*', (req, res) => {
res.status(404).send('404 - Page not found');
});
app.listen(port, () => {
console.log(`Express server listening on port ${port}`);
});
| Route | Description |
|---|---|
app.get('/users/:userId', ...) |
GET /users/42 — :userId is a route parameter accessed via req.params.userId |
app.post('/', ...) |
POST / — handles form submissions, data uploads, etc. |
app.get('/about', ...) |
GET /about — static page route |
app.all('*', ...) |
Matches any method and path not matched above — use for 404 |
app.listen(port, callback)starts the server. Without this line the server never starts — this was the missing piece in the lecture slide code!
Click ▶ Run. The HTTP Client is pre-filled with GET /about. Try also:
GET http://localhost:8080/users/42GET http://localhost:8080/nonexistent (should return 404)const express = require('express');
const app = express();
const port = 8080;
// Route parameter: GET /users/:userId
app.get('/users/:userId', (req, res) => {
res.send(`GET request to user ${req.params.userId}`);
});
// POST /
app.post('/', (req, res) => {
res.send('POST request to the homepage');
});
// GET /about
app.get('/about', (req, res) => {
res.send('About page');
});
// Catch-all 404 handler — must be last
app.all('*', (req, res) => {
res.status(404).send('404 - Page not found');
});
// app.listen() was missing from the lecture slide — added here!
app.listen(port, () => {
console.log(`Express server listening on port ${port}`);
});
1. What happens if you move the app.all('*', ...) catch-all handler to the top of the file, before all other route definitions?
Express matches routes in the order they are registered. If app.all('*', ...) is first, it matches every incoming request and sends the 404 response — none of the subsequent routes are ever reached. Route order is a fundamental Express concept: always place more specific routes before catch-alls.
2. You need to add a DELETE /users/:userId route that removes a user and responds with { "deleted": true, "id": "<userId>" } as JSON. Which implementation is correct?
REST convention maps the DELETE HTTP verb to app.delete(). The route parameter :userId is read from req.params.userId. res.json() serialises the object and sets Content-Type: application/json. Option B uses GET for a destructive operation (violates REST semantics). Option C uses res.send() with an object — Express serialises it, but res.json() is explicit and idiomatic. Option D uses POST and omits the user ID from the response body.