What this chapter is really for
Slide 1 is a leftover chapter 5 task (make a table appear when a heading is clicked using DOM manipulation) — do it, then start here.
The deck moves through four stages, and each one exists because the previous one hit a wall:
- Node basics (3–14) — modules,
fs,http. You build a server by hand and immediately see how tedious routing is. - Async (15–28) — why Node is non-blocking, callback hell, promises,
fetch. Because the raw callbacks in stage 1 do not scale. - npm and Express (29–45) — packages, middleware, routing, three ways to receive parameters. Because hand-written routing does not scale either.
- EJS (46–50) — generating HTML from data on the server.
The idea that makes Node make sense
Node is non-blocking, asynchronous and single-threaded. One worker services every request in one event loop, delegating slow work to other agents. That single sentence explains the advantages, the disadvantages, and why every file operation takes a callback.
Where it connects
Chapter 1’s HTTP methods and ports become app.get and
app.listen. Chapter 2’s form action and method
finally point somewhere real. Chapter 4’s callbacks become promises. And chapter 7 plugs a
database into the routes you write here.
All 51 slides, weighted
| Slides | Topic | Weight | What to do with it |
|---|---|---|---|
| 1 | Chapter 5 leftover task | Write it | DOM manipulation to show a table on click. Ten minutes, and it revises the previous chapter. |
| 2–4 | Introducing Node; JavaScript everywhere | Memorize | Five advantages: JavaScript everywhere, push architectures, non-blocking architectures, a rich tool ecosystem, broad adoption. |
| 5–6 | Name conflicts; what a module is | Memorize | There is no function overloading in JavaScript — the second declaration simply replaces the first. Modules solve the resulting name conflicts. |
| 7–8 | Running a Node app; import/export | Write it | node app.js or just node app; Ctrl-C to stop. module.exports and require. |
| 9 | Node core modules | Memorize | Six to know: http, url, querystring, path, fs, util. |
| 10–14 | fs module; http module; simple and static servers | Write it | Type the simplest HTTP server from memory. It is nine lines and it is the most likely practical question in the first half. |
| 15–18 | Blocking vs non-blocking; high volume; disadvantages | Memorize | The restaurant analogy is the exam answer. Learn both disadvantages: relational databases were awkward, and computation-heavy work blocks the single thread. |
| 19–21 | Asynchronous coding; the async fs example; callback hell | Memorize | Slide 21 is the deliberate demonstration of callback hell — nested readFiles so the second can see the first result. |
| 22–23 | Promises | Write it | The single most important construct in the chapter. Know resolve/reject, then/catch, and how to wrap a callback API in one. |
| 24–28 | fetch; what it returns; common mistakes | Write it | fetch does not return the data — it returns a Promise. A web API is a web resource that returns data instead of HTML, CSS, JavaScript or images. |
| 29 | npm, package.json, dependencies | Memorize | npm init -y then npm install express. Know what each does. |
| 30 | CORS | Memorize | Browsers block cross-origin requests by default. Access-Control-Allow-Origin: *, or app.use(cors()) in Node. |
| 31–33 | Semantic versioning; .gitignore; dev dependencies | Memorize | MAJOR.MINOR.PATCH, and what ~ and ^ each allow. Never commit node_modules. |
| 34–36 | Express, static files, middleware | Write it | Middleware is the chain of responsibility pattern. app.use() installs a function on that chain. |
| 37–38 | Routing; chained middleware | Write it | The four-handler pattern — static, two routes, and a catch-all 404 — is the shape of every Express app you will write. |
| 39–40 | Environment variables; a simple API | Write it | dotenv, a .env file, and process.env.PORT. Used in every later example. |
| 41–42 | Separating functionality into modules | Write it | Once you have five or six routes a single file becomes too complex. This is the refactor pattern for the project. |
| 43–45 | Three ways to receive parameters | Write it | Route params :word, query params ?first=, and form bodies. Know which object each lands in. |
| 46–50 | View engines and EJS, including partials | Write it | Learn the two tag forms — <% %> runs code, <%= %> prints a value — and the <%- include %> partial. |
| 51 | Supporting material | Skim | Installation links and a video walkthrough. |
Modules, files and a server by hand
Why modules exist
// JavaScript has NO FUNCTION OVERLOADING. The second declaration // simply replaces the first — silently. let product = (x, y) => x * y; let product = (x, y, z) => x * y * z; console.log(product(2, 3)); // NaN — z is undefined, and 6 * undefined = NaN // With hundreds of literals across dozens of .js files, you need some // way to prevent name conflicts. That way is MODULES: literals defined // within a module are SCOPED TO THAT MODULE.
// ── mod-names.js ─────────────────────────────────────────────── const secret = 'SECRET PHRASE'; // local — NOT exported, stays private const first_name = 'salah'; const last_name = 'abid'; module.exports = { first_name, last_name }; // export an OBJECT // ── mod-utils.js ─────────────────────────────────────────────── const saySelem = (name) => { console.log(`Selem Mr ${name}`); }; module.exports = saySelem; // export a FUNCTION (default style) // ── app.js ───────────────────────────────────────────────────── const names = require('./mod-names'); // ./ — a LOCAL file, not an npm package const selem = require('./mod-utils'); selem(names.first_name); // "Selem Mr salah" selem(names.secret); // "Selem Mr undefined" ← not exported = not visible // RUN IT: node app.js or just node app // STOP IT: Ctrl-C // On the CLIENT side you must tell the browser a file is a module: // <script src="art.js" type="module"></script> // In Node every file is a module by default (CommonJS).
Core modules
| Module | What it gives you |
|---|---|
http | Classes, methods and events to create a Node HTTP server. |
url | Methods for URL resolution and parsing. |
querystring | Methods to deal with query strings. |
path | Methods to deal with file paths. |
fs | Classes, methods and events for file I/O. |
util | Utility functions. |
The fs module — synchronous first
const { readFileSync, writeFileSync } = require('fs'); // destructured import console.log('start'); const first = readFileSync('./content/first.txt', 'utf8'); const second = readFileSync('./content/second.txt', 'utf8'); writeFileSync( './content/result.txt', `Here is the result : ${first}, ${second}` + "\n", { flag: 'a' } // 'a' for APPENDING (default would overwrite) ); console.log('Task completed!'); // OUTPUT: start // Task completed! // …in that order, because Sync versions BLOCK until they finish.
A server by hand — the http module
const http = require('http'); const server = http.createServer((req, res) => { res.write("This is my response to your request!"); res.end(); // end() is REQUIRED — without it the browser hangs return; }); server.listen(5000); console.log("Listening on port 5000"); // > node simplest-server // Then open http://127.0.0.1:5000 in the browser. // // NOTE: http://127.0.0.1:5000/anything/at/all gives the SAME response. // There is no routing here at all — which is the point of the next slide.
const http = require('http'); const server = http.createServer((req, res) => { if (req.url === '/') { res.end("This is the homepage!"); } else if (req.url === '/about') { res.end("This is the ABOUT page!"); } else { res.end(`<h1>Page not Found!</h1> <p><a href="/">Homepage</a></p>`); } return; }); server.listen(5000); // A fuller version writes headers explicitly: // res.writeHead(200, {"Content-Type": "text/plain"}); // // Now imagine twenty routes in this if/else chain. That is why Express // exists — and why slide 34 arrives when it does.
Non-blocking architecture, promises and fetch
Blocking vs non-blocking — the restaurant analogy
| Blocking (Apache with JEE or PHP) | Non-blocking (Node) |
|---|---|
| A blocking multiprocessing or multithreaded model. | A single worker services all requests in a single event loop thread. |
| As though a single person had to handle every task for each table — so you need one person per table. | The worker can only do one thing at a time, but delegates other tasks to other agents and carries on. |
| Scales by adding processes or threads. | Node is non-blocking, asynchronous and single-threaded. |
Where Node shines: data-intensive real-time applications talking to distributed computers, with NoSQL data sources. The slide’s example is a Like button handling a massive number of concurrent writes — a memory-based message queue records the changes and they are persisted eventually.
Where Node does not: sites whose data lives in a traditional relational database such as MySQL, where access was a complex programming task; and computationally heavy work such as video processing or scientific computing, which stalls the single thread.
Callback hell — the problem, demonstrated
const { readFile, writeFile } = require('fs'); // the ASYNC versions console.log('Starting task A...'); readFile('./content/first.txt', 'utf8', (err, result) => { if (err) { console.log(err); return; } // error-first callback convention writeFile('./content/result-async.txt', `Here is the Async result : ${result}`, (err, result) => { if (err) { console.log(err); return; } console.log('Task A completed!'); } ); }); console.log('starting next task...'); // OUTPUT ORDER — the whole point: // Starting task A... // starting next task... ← this line runs BEFORE the file is read // Task A completed! ← the callback fires last
// To let the second read see the first result, the second readFile has // to be called INSIDE the first callback. Add a third file and a fourth // and you get a staircase drifting off the right of the screen. readFile('./content/first.txt', 'utf8', (err, result) => { if (err) { console.log(err); return; } const first = result; readFile('./content/second.txt', 'utf8', (err, result) => { if (err) { console.log(err); return; } const second = result; writeFile('./content/result-async.txt', `Here is the result : ${first}, ${second}`, (err, result) => { if (err) { console.log(err); return; } console.log('done with this task'); } ); }); }); // ← ← ← CALLBACK HELL → → →
Promises — the solution
A Promise object represents the eventual completion (or failure) of an asynchronous operation and its resulting value. Probably the promise will complete and you receive the data; or it will not, and you receive an error instead.
// ── DECLARATION AND INSTANTIATION ────────────────────────────── // The handler passed to the constructor takes TWO parameters. const promiseObj = new Promise((resolve, reject) => { doWork(); if (someCondition) resolve(someValue); // works like a return else reject(someMessage); // works like a throw }); // ── CONSUMING IT ─────────────────────────────────────────────── promiseObj .then(someValue => { // success — the promise was achieved }) .catch(someMessage => { // the promise was not satisfied }); // ── WRAPPING A CALLBACK API IN A PROMISE (slide 23) ──────────── const { readFile } = require('fs'); const read = (path) => { return new Promise((resolve, reject) => { readFile(path, 'utf8', (err, data) => { if (err) reject(err); else resolve(data); }); }); }; read('./content/first.txt') .then(data => console.log(data)) .catch(err => console.log(err)); // ── CHAINING beats nesting — the whole payoff ────────────────── read('./content/first.txt') .then(first => read('./content/second.txt')) .then(second => console.log(second)) .catch(err => console.log(err)); // ONE catch for the whole chain
fetch — asynchronous data requests
A web API is simply a web resource that returns data instead of HTML, CSS,
JavaScript or images. fetch is done from the client side to get that data
asynchronously, and it replaces the older AJAX technology.
let cities = fetch('/api/cities?country=italy'); // What does `cities` contain? NOT the JSON data. It takes time for the // service to execute and respond, so fetch returns a PROMISE object. fetch('/api/cities?country=italy') .then(response => { console.warn('response received!!!'); return response.json(); // ← ALSO returns a promise }) .then(data => { console.log(data); // finally the actual data }) .catch(err => console.log(err)); // COMMON MISTAKES (slide 28): // 1. expecting fetch to return the data directly // 2. forgetting that response.json() is ALSO asynchronous // 3. nesting multiple fetches inside each other — callback hell again. // CHAIN them with .then, or use async/await: async function load() { try { const response = await fetch('/api/cities?country=italy'); const data = await response.json(); console.log(data); } catch (err) { console.log(err); } }
fetch: it returns a Promise, not data — and response.json() returns another one. Two awaits, or two .then()s. Nearly every broken fetch in a lab is one of those two missing.CORS
// Modern browsers PREVENT cross-origin requests by default, which makes // legitimate sharing between two domains harder. // // An ORIGIN is a protocol + domain + port. All three must match. // https://a.com vs http://a.com ← different (protocol) // https://a.com vs https://b.com ← different (domain) // http://a.com:3000 vs http://a.com:5000 ← different (port) // An API that wants to allow ANY domain adds this header to responses: // Access-Control-Allow-Origin: * // In Node: // > npm install cors const cors = require('cors'); app.use(cors()); // allows the API to be accessed through JavaScript
Packages, routing, middleware and parameters
npm and package.json
# npm = Node Package Manager npm init -y # creates a basic package.json (-y skips the questionnaire) npm install express # installs into node_modules AND adds it to dependencies npm install nodemon -D # a DEV dependency — needed at development time only npm install -g nodemon # or globally npm install # restores node_modules from package.json npm run start # runs the "start" script npm run dev # runs the "dev" script — nodemon restarts on save // package.json { "name": "05-npm-demo", "version": "1.0.0", "main": "index.js", "scripts": { "start": "node app.js", "dev": "nodemon app.js" }, "dependencies": { "express": "^4.18.2" }, "devDependencies": { "nodemon": "^2.0.20" } } # .gitignore — node_modules is NEVER shared. Teammates recreate it # from package.json with `npm install`. /node-modules
Semantic versioning — MAJOR.MINOR.PATCH
| Part | Increments when… | Breaks your code? |
|---|---|---|
| MAJOR | You make incompatible API changes. | Yes — will not work with earlier versions. |
| MINOR | You add functionality in a backwards-compatible way. | No. |
| PATCH | You make backwards-compatible bug fixes. | No. |
~1.3.8 | Allows automatic update to the latest PATCH. | No. |
^1.3.8 | Allows automatic update to the latest MINOR. | No. |
Express: static files, routing and the 404
const express = require('express'); const app = express(); // A — the public folder becomes visible to HTTP requests, so // http://localhost:3000/css/styles.css works if css/ is inside public/ app.use( express.static('public') ); // B — a route is a URL: a series of folders, files, or parameter data app.get('/', (request, response) => { response.sendFile('./pages/index.html'); }); // C app.get('/about', (request, response) => { response.sendFile('./pages/about.html'); }); // D — ANY other request falls through to here. Order matters: // this must come LAST or it swallows everything. app.use( (request, response) => { response.status(404).sendFile('./pages/404.html'); }); app.listen(3000, () => { console.log('listening on 3000'); }); // A request tries A, then B, then C, then D — the FIRST match wins.
Middleware — the chain of responsibility
Middleware is software that is a bridge between an application and the data, and Express arranges it as a chain of responsibility. The slides give the reason to use it: it splits server operations into smaller units, such as performing validation on the data, which leads to better app structure and reuse — and you can block the execution of the current chain and pass control to functions that handle errors.
// app.use() INSTALLS a middleware function on the chain. // At the ROOT of the route — runs for EVERY request app.use( (req, res, next) => { console.log(`${req.method} ${req.path} - ${req.ip}`); next(); // ← WITHOUT next() the request HANGS FOREVER }); // Mounted at a SPECIFIC route: app.METHOD(path, middleware, callback) app.get('/now', function (req, res, next) { req.time = new Date().toString(); // attach data to the request next(); // hand on to the next function }, function (req, res) { res.json({ time: req.time }); // the last one SENDS the response } ); // Built-in and third-party middleware you have already met: app.use(express.static('public')); // serve static files app.use(cors()); // allow cross-origin app.use(bodyParser.urlencoded({ extended: false })); // parse form bodies
next(). There is no error, no crash, no log — the browser just spins until it times out. If a request hangs, look for a missing next() before you look anywhere else.Environment variables
# .env — any number of key=value pairs PORT=8080 BUILD=development // > npm install dotenv require('dotenv').config(); // load .env into process.env console.log(process.env); // see everything available console.log("build type=" + process.env.BUILD); app.listen(process.env.PORT); // never hard-code the port again // .env belongs in .gitignore too — it holds secrets.
A simple API, and splitting it into modules
// ── data-module.js — slide 42 ────────────────────────────────── const fs = require('fs'); const path = require('path'); const jsonPath = path.join(__dirname, 'data', 'SE2022.json'); // __dirname = the folder THIS file is in. path.join builds a path // that works on Windows and Linux alike. let curriculum; fs.readFile(jsonPath, (err, data) => { if (err) console.log('Unable to read json data file'); else curriculum = JSON.parse(data); }); const getData = () => { return curriculum; }; module.exports = getData; // ── app.js ───────────────────────────────────────────────────── const express = require('express'); require('dotenv').config(); const getData = require('./data-module'); const app = express(); app.get('/', (req, resp) => { resp.json(getData()); }); // res.json sends JSON app.listen(process.env.PORT, () => { console.log("Listening at port… " + process.env.PORT); }); // WHY: with five or six routes a single Node file becomes too complex. // Separate the routing, and separate the handler logic, into modules.
Three ways to receive parameters — know which object each lands in
| Way | URL / source | Read it from | Needs |
|---|---|---|---|
| Route params | /echo/hello | req.params.word | The :param syntax in the route. |
| Query params | /employee?first=skander&last=turki | req.query.first | Nothing — no library needed. |
| Form body | A POSTed <form> | req.body.first | body-parser middleware. |
// ── 1. ROUTE PARAMS — the :param syntax (slide 43) ───────────── app.get('/echo/:word', (req, res) => { let param = req.params.word; res.json({ echo: param }); }); // GET https://ip:port/echo/hello → 'hello' lands in req.params.word // ── 2. QUERY PARAMS — URL-encoded queries (slide 44) ─────────── app.get('/employee', (req, res) => { res.json({ employee: `${req.query.first} ${req.query.last}` }); }); app.post('/employee', (req, res) => { res.json({ employee: `${req.query.first} ${req.query.last}` }); }); // http://ip:port/employee?first=skander&last=turki // ── 3. FORM BODY (slide 45) ──────────────────────────────────── // <form action="/name" method="post"> // <label>First Name :</label><input type="text" name="first"><br> // <label>Last Name :</label><input type="text" name="last"><br> // <input type="submit" value="Submit"> // </form> // A payload (POST, DELETE…) needs middleware to be parsed: const bodyParser = require('body-parser'); app.use(bodyParser.urlencoded({ extended: false })); app.post('/name', (req, res) => { let p_first = req.body.first; // ← req.BODY, not query, not params res.send(`Hello ${p_first}`); }); // Modern Express has this built in: app.use(express.urlencoded({extended:false}))
View engines: generating HTML on the server
A view engine lets Node generate HTML from data. Install the package, tell Express which engine to use and which folder holds the views, and render instead of sending files.
// > npm install ejs // register the view engine app.set('view engine', 'ejs'); // templates go in /views by DEFAULT — configure only if you want another app.set('views', 'otherThanViewsFolder'); // response.render() sends the template — 'index' means views/index.ejs app.get('/', (request, response) => { response.render('index'); });
const express = require('express'); const app = express(); app.set('view engine', 'ejs'); app.use(express.static('public')); app.get('/', (req, res) => { const blogs = [ { title: 'First post', snippet: '…' }, { title: 'Second post', snippet: '…' }, ]; // SECOND ARGUMENT = the data sent to the template res.render('index', { title: 'Home', blogs: blogs }); }); app.get('/about', (req, res) => res.render('about', { title: 'About' })); app.get('/create', (req, res) => res.render('create', { title: 'Create a new blog' })); // catch-all 404, rendered rather than sent as a file app.use((req, res) => { res.status(404).render('404', { title: '404' }); }); app.listen(3000);
The two EJS tag forms — and partials
<html lang="en"> <%- include("./partials/head.ejs") %> <body> <%- include("./partials/nav.ejs") %> <div class="blogs content"> <h2><%= title %>: All Blogs</h2> <% if (blogs.length > 0) { %> <% blogs.forEach(blog => { %> <h3 class="title"><%= blog.title %></h3> <p class="snippet"><%= blog.snippet %></p> <% }) %> <% } else { %> <p>There are no blogs to display...</p> <% } %> </div> <%- include("./partials/footer.ejs") %> </body> </html> <!-- THE THREE TAGS: <% %> runs JavaScript, outputs NOTHING (if, forEach, loops) <%= %> outputs the VALUE of an expression (escaped — safe) <%- %> outputs UNESCAPED — used for include() PARTIALS let one .ejs file be included in another so shared parts are reused. Organise them under views/partials/ : /views/partials/head.ejs /views/partials/header.ejs /views/partials/footer.ejs -->
<% %> versus <%= %>. Anything that controls flow uses the plain form; anything that appears on the page needs the equals sign. Writing <% title %> renders nothing at all, silently.The eight things people get wrong
next()The request hangs. No error, no stack trace, no log line — the browser spins until it times out. The chain of responsibility simply stopped.
Fix: Every middleware function either calls next() or sends a response. Exactly one of the two, never neither.
app.use((req,res) => res.status(404)...) written before your routes catches every request, and your real routes never run.
Fix: Order is everything in Express: static, then specific routes, then the catch-all last. The first match wins.
fetch to return datalet cities = fetch(url) gives you a Promise object, not the cities. Logging it shows Promise { <pending> } and people conclude the API is broken.
Fix: Chain .then(r => r.json()).then(data => ...), or await twice. Slide 26 asks this exact question.
req.queryA POSTed form puts its data in the request body, not the URL. req.query.first is undefined and so is req.body.first until the parser is installed.
Fix: Three sources, three objects: req.params for :route params, req.query for ?query= strings, req.body for form payloads — and the last one needs body-parser or express.urlencoded.
node_modulesThousands of files in the repository, huge diffs, and merge conflicts in code nobody wrote. Slide 32 is explicit that the folder does not have to be shared.
Fix: Add /node_modules to .gitignore. Teammates run npm install and get the same tree from package.json.
app.listen(3000) works on your laptop and fails wherever the platform assigns a port. It also means every teammate edits the same line.
Fix: require('dotenv').config() and app.listen(process.env.PORT). Put .env in .gitignore as well — it holds secrets.
The request works in Postman and in the address bar but fails from your page. That is the browser enforcing the same-origin policy, not the API refusing you.
Fix: The server must send Access-Control-Allow-Origin. In your own Node app: npm install cors, then app.use(cors()). Remember an origin is protocol + domain + port — a different port is a different origin.
<% %> where <%= %> was needed<h2><% title %></h2> renders an empty heading. The tag evaluated the expression and threw the result away.
Fix: <% %> runs code. <%= %> prints a value. <%- %> prints unescaped, which is what include() needs.
Chapter 6 on a single screen
Modules
module.exports = { a, b }module.exports = fnconst x = require('./file')- Not exported = not visible
- Client side:
<script type="module">
Core modules
http— create a serverurl— parse URLsquerystring— query stringspath— file pathsfs— file I/Outil— utilities
Terminal
node app·Ctrl-Cnpm init -ynpm install expressnpm install nodemon -Dnpm install— restore modulesnpm run dev
Async
- Node: non-blocking, async, single-threaded
- Callback:
(err, result) => {} new Promise((resolve, reject) => {}).then()·.catch()awaitinsideasync functionfetchreturns a Promise, not data
Express
app.use(express.static('public'))app.get(path, (req, res) => {})app.postapp.putapp.deleteres.sendres.jsonres.sendFileres.renderres.status(404)app.listen(process.env.PORT)
Middleware
- Chain of responsibility
app.use((req,res,next) => { next(); })- Always
next()or send a response - Order matters — 404 handler goes LAST
cors()·bodyParser.urlencoded()
Parameters
/echo/:word→req.params.word?first=x→req.query.first- POSTed form →
req.body.first - Bodies need a parser middleware
EJS
app.set('view engine', 'ejs')- Templates live in
/views res.render('index', { title })<% %>run ·<%= %>print<%- include("./partials/head.ejs") %>
SemVer
MAJOR.MINOR.PATCH- MAJOR — breaking changes
- MINOR — new features, compatible
- PATCH — bug fixes, compatible
~1.3.8latest patch ·^1.3.8latest minor
Build one app, one slide at a time
This chapter cannot be revised from paper — every idea in it is something a server either does or fails to do. Work through the list as one growing project rather than fifteen throwaway files, and by the end you will have the skeleton chapter 7 needs.
- Do the slide 1 leftover task first: show a table when a heading is clicked, using DOM manipulation methods.
- Write two modules and an app that imports both. Deliberately leave one constant unexported and confirm it reads as
undefined. - Read two files with
readFileSyncand append the result to a third with{ flag: 'a' }. - Write the simplest HTTP server from memory in nine lines, then visit a nonsense URL and confirm it gives the same response.
- Add hand-written routing with
req.url, including a 404 branch. Then imagine twenty routes and decide you want Express. - Rewrite the same two-file read asynchronously and log the output order — predict it before running.
- Nest a second
readFileinside the first callback so it can see the first result. That is callback hell; look at the indentation. - Wrap
readFilein a Promise and chain two reads with.then. Compare the shape to the previous drill. - Fetch from a public API, log the raw
fetchreturn value, then fix it with two.thens, then rewrite withasync/await. - Run
npm init -y, install express and nodemon (one as a dev dependency), and read the resultingpackage.jsonline by line. - Add
startanddevscripts, runnpm run dev, and edit a file while it is running. - Build the four-handler Express app: static, two routes, catch-all 404. Then move the 404 to the top and watch everything break.
- Add a logging middleware that prints method, path and IP. Then delete its
next()and watch the request hang. - Move the port into a
.envfile and read it withdotenv. - Write three endpoints that take a parameter three different ways — route, query, and a POSTed form — and note which
reqobject each uses. - Split the routes and the data loading into separate modules, as slides 41–42 describe.
- Convert the pages to EJS templates, pass real data into one, and factor the header and footer into partials.
There is no chapter-6 example folder in your study material, but two solved labs cover exactly this
ground: lab 09 — Express and EJS
and lab 08.
Slide 51 and chapter 3 slide 85 both point to the full code repository at
github.com/skanderturki/se371.