Appearance
Week 1: Servers & Express Intro
Lecture notes: Week 01: Course Intro, Express Intro (Sept 8) · In-class code: Week 1 NYT (server.js)
Overview
This week builds a working web server from scratch in about 12 lines. You create a Node.js project (Node runs JavaScript outside the browser), install Express (a popular Node library for building web servers), and write route handlers that answer HTTP requests. You then learn the three ways a client can send data to a server (request body, query string, route parameters) and how to answer with an appropriate status code when the request is bad. Postman comes in as the tool for sending requests a browser can't easily make.
Why it matters: every backend is, at its core, a program that waits for requests and sends back responses. Everything later in the course (routers, middleware, databases, REST design) is built on the app.get(path, (req, res) => …) pattern you write this week.
About the code on this page
The Week 1 in-class code is plain JavaScript (server.js). TypeScript arrives in Week 2. The Page 0 concepts still apply: destructuring and arrow functions are all over this file. So are template literals: strings in backticks where ${expression} inserts a value, like `Hello ${name}!`.
Key concepts
The client–server model
The client–server model splits work between clients, which request a service, and servers, which provide it. The client always starts the conversation with a request, and the server answers with a response. They need an agreed-on set of rules for that conversation (a protocol), and on the web that protocol is HTTP (HyperText Transfer Protocol). HTTP defines what a request looks like (a method such as GET or POST, a path such as /about, and optional data) and what a response looks like (a status code and content).
A few points from the reading that are easy to overlook:
- "Client" and "server" are roles played by software, not types of hardware. Your laptop runs a server (
localhost:3000) and a client (your browser, or Postman) at the same time this week. - One server handles many clients, so it has to juggle many requests.
- The model is centralized, which makes it easier to control and update, but the server becomes a single point of failure and a potential bottleneck. The alternative is peer-to-peer, where every node is both client and server.
🥀 Real life: the deli counter
You (the client) walk up and ask for half a pound of turkey. The person behind the counter (the server) makes it and hands it over. They never walk into the store and start handing sandwiches to people who didn't ask. The server only ever responds. And one counter serves the whole line of customers, which is why it gets slow at lunchtime.
Web servers vs. web applications
A web server is software that accepts HTTP requests and returns responses. It can serve static content (files that already exist, like an image or an HTML page) or dynamic content (generated by a program at the moment of the request). Apache and Nginx are classic web servers.
A web application is software that runs through the browser and does real work on the server side, generating responses based on what the user did. The reading describes the classic three-tier structure:
| Tier | What it is | In this course |
|---|---|---|
| Presentation | what the user sees (the browser) | Postman / a browser, for now |
| Application | the logic that builds responses | your Express code |
| Storage | the database | MongoDB, from Week 3 |
This course lives almost entirely in the application tier, with the storage tier arriving in Week 3.
🥀 Real life: vending machine vs. barista
A static server is a vending machine: the snacks are already made, and you get exactly what's in slot B4. A dynamic web app is a barista: your oat-milk half-caf is made for your order, at the moment you ask. Express handlers are baristas.
Setting up an Express project
npm is Node's package manager. It downloads libraries ("packages") like Express and records them in package.json, the file that describes your project: its name, its dependencies, and its scripts (named shortcut commands you run with npm run <name>, or npm start for the start script).
The lecture's setup steps:
bash
mkdir week1hello && cd week1hello
npm init -y # creates package.json with default answers
npm install express@latest # adds express to "dependencies"Then edit package.json:
json
{
"type": "module",
"main": "server.js",
"scripts": {
"start": "node --watch server.js"
},
"dependencies": {
"express": "^5.2.1"
}
}"type": "module"lets you use modernimport express from 'express'instead ofrequire(...)(see modules on Page 0).node --watchrestarts the server whenever you save, sonpm startgives you a live-reloading server.
Routes: app.METHOD(path, handler)
A route is a combination of an HTTP method and a path. The method says what the client wants to do: GET means "give me something", and POST means "here's data, do something with it" (Week 2 adds PATCH and DELETE). A route handler is the function Express calls when a request matches both the method and the path:
js
import express from 'express';
const app = express();
const port = 3000;
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});req(request) is everything the client sent.res(response) is how you answer:res.send(...),res.json(...),res.status(...).app.listen(port)actually starts the server. Until that runs, nothing is listening. A port is a numbered "door" on a computer that a server listens on, solocalhost:3000means "port 3000 on this machine".
If you visit a path with no matching route, Express answers with a 404 (Cannot GET /about). Routes are also method-specific: after defining app.post('/submit', …), visiting /submit in the browser still gives Cannot GET /submit, because a browser address bar always sends a GET.
Three ways data reaches your handler
This is the core idea of the week:
| Where | Looks like | Read it with | Typical use |
|---|---|---|---|
| Route params | /person/33, /play/artist/Radiohead/song/Creep | req.params | which resource: an id, a name |
| Query string | /people?letter=c, /catalog?itemid=222 | req.query | options: search, filter, sort |
| Request body | a form submission or JSON sent with POST | req.body (needs middleware) | the data you're creating or changing |
Query strings come after a ? in the URL, as key=value pairs joined by &. Special characters are percent-encoded: %20 is a space. Express decodes them for you, so ?name=The%20Clash arrives as "The Clash".
Route params are named placeholders in the path, marked with :. A single route can have several:
js
app.get('/league/:leaguename/team/:teamname', (req, res) => {
const { leaguename, teamname } = req.params;
res.send(`You want to know about the ${teamname}? They're in the ${leaguename}!`);
});🥀 Real life: mailing a package
The route params are the address, telling the mail system which door it's going to. The query string is a note on the outside ("leave at back door", "signature required"): options about how to handle it. The body is what's inside the box. You can't see the contents from the outside, and someone has to open it (middleware) before you can use it.
The request body needs middleware
The body doesn't arrive as a neat JavaScript object. The lecture tries console.log(req) and finds a huge object with no obvious form fields. Middleware is code that runs before your handler and transforms the request. Two built-in parsers cover this week:
js
app.use(express.urlencoded()); // HTML form submissions → req.body
app.use(express.json()); // JSON bodies (e.g. from Postman) → req.bodyRegister them with app.use() before the routes that need them. Without the right one, req.body is undefined and destructuring it throws. Week 2 goes much deeper into middleware.
Status codes
A status code is the three-digit number at the start of every HTTP response that says how the request went. The 2xx codes mean success, 4xx mean the client did something wrong, and 5xx mean the server failed. If you don't set one, Express sends 200 OK. When the client sends something invalid, say so with res.status(code) before sending:
js
app.get('/person/:id', (req, res) => {
const { id } = req.params;
if (isNaN(Number(id))) {
res.status(400).send('BAD REQUEST: Person ID must be a number');
return; // stop here, or the next line runs too
}
res.send(`You asked for person ${id}`);
});The lecture's advice: pick codes that are informative to the client. 500 ("something broke on our end") is vague, and very specific codes like 416 rarely fit. Week 2 has the full table of the ones you'll use.
Postman
Browsers make GET requests easily, but POST, PATCH, and DELETE are awkward to send from one. Postman sends any HTTP request with any headers and body, and shows you the response. The lecture sets up a collection (SI 679 → Week1) with a GET to http://localhost:3000/ and a POST to /postData with a raw JSON body.
Code walkthrough
Full source: server.js · package.json
This is the "Now You Try" exercise: implement routes that produce given responses.
Query string + validation: /catalog?itemid=222
js
app.get('/catalog', (req, res) => {
const { itemid } = req.query; // "222" (always a string, or undefined)
if (isNaN(Number(itemid))) { // "xyz" → NaN → reject
res.status(400).send("Error: itemid must be a number");
return; // don't fall through to the success reply
}
res.send(`Oh, you're looking for item ${itemid}?`);
});- Query values arrive as strings, so
Number(itemid)converts before checking. returnafter sending the 400 matters. Without it, Express would try to send a second response and throwCannot set headers after they are sent.
Several route params: /play/artist/:artistName/song/:songName
js
app.get('/play/artist/:artistName/song/:songName', (req, res) => {
const { artistName, songName } = req.params; // destructure both at once
res.send(`Oh, you want to listen to ${songName} by ${artistName}?`);
});/play/artist/The%20Clash/song/London%20Calling arrives already decoded: artistName is "The Clash".
Serving a form: /getform
The handler sends HTML as a string with a form that POSTs to /submitform. The lecture notes point out that this is an awful way to build a real site; it's only for illustration. Three problems in this version:
html
<form action=’/submitform’ method="post"> <!-- ① curly quotes -->
…
<input type="text" id="city" name="city" placeholder="01234"/> <!-- ② zip reuses "city" -->’/submitform’uses typographic quotes (’), not'or". HTML doesn't treat them as quotes, so the action becomes the literal text’/submitform’and the form posts to the wrong URL. This is easy to get when copying from a doc or slide.- The zipcode input reuses
id="city" name="city", so the zip would overwrite the city inreq.body. - There's no
app.post('/submitform', …)handler yet (see Practice problem 2).
JSON in, JSON out: POST /tracks
js
app.post('/tracks', (req, res) => {
const { artist, title, album, year } = req.body; // needs express.json() above
let response = {
status: "success",
trackAdded: { artist: `${artist}`, title: `${title}`, album: `${album}`, year: `${year}` },
timestamp: new Date().toISOString(), // "2026-09-08T19:30:51.899Z"
};
res.send(JSON.stringify(response));
});app.use(express.json())at the top of the file is what makesreq.bodyexist.- Wrapping each field in a template literal (
`${artist}`) turns a missing field into the string"undefined", so the response says"artist": "undefined"instead of reporting an error. The lecture's second "Now You Try" wants a 400 withartist must be a valid, non-empty stringinstead (see Practice problem 3). res.send(JSON.stringify(response))sends the right text but with the wrong Content-Type. That's a label attached to the response (a header, covered in Week 2) that tells the client what format the body is in, and here it saystext/html.res.json(response)does the stringify and setsContent-Type: application/json, which is what API clients expect.
Readings summary
Video: introduction to web servers (YouTube)
Not summarized yet
This reading is a video (youtube.com/watch?v=eesqK59rhGA), so its content couldn't be pulled into these notes automatically. Add a summary here after watching it.
Wikipedia: Client–server model
Defines the model as splitting work between service providers (servers) and service requesters (clients) that communicate over a network with an agreed protocol. It stresses that the roles are about software: one machine can be both. It compares the model with peer-to-peer systems, where every node is equal, and weighs centralization: easier control and simpler clients, but dependency on the server and possible bottlenecks.
Connection to lecture: Express is the server role, and Postman or the browser plays the client. The request/response cycle is literally the (req, res) pair in every handler.
Wikipedia: Web server
A web server is software (and the hardware running it) that accepts HTTP requests and returns resources or error messages. It separates static content (existing files: fast and easily cached) from dynamic content (generated per request). It lists common features: logging, virtual hosting, access control, URL rewriting, custom error pages, caching. It also covers performance: requests per second, response time, and what happens under overload.
Connection to lecture: app.listen(3000) turns your script into a web server. Every handler you write produces dynamic content. Features the reading lists, like logging, access control, and custom error pages, are exactly what Week 2's middleware implements.
Wikipedia: Web application
A web application runs in the browser with its work done on a server, so it needs no installation and updates centrally. The article describes the three-tier architecture (presentation, application, storage) and notes that more complex apps split the application tier further ("n-tier").
Connection to lecture: the course builds the application tier. The "n-tier" idea returns in Week 4, where one Express app is split into routes → controllers → services → models → db.
Gotchas & common mistakes
- Forgetting
"type": "module". Thenimport express from 'express'fails withCannot use import statement outside a module. - Middleware after the route.
app.use(express.json())must come before the routes that readreq.body. Otherwisereq.bodyisundefined. - Wrong body parser. HTML forms need
express.urlencoded(). Postman's raw JSON needsexpress.json(). In Postman, the body type dropdown must be set to JSON. - Sending two responses. Forgetting
returnafterres.status(400).send(...)leads toCannot set headers after they are sent to the client. - Testing POST in the browser. Typing a URL into the address bar is always a GET, so
Cannot GET /submitdoesn't mean your POST route is broken. - Missing query params crash code. The lecture's
/peopleroute callsletter[0].toLowerCase(), and with no?letter=,letterisundefinedand the server throws (the client gets a 500). Check that it exists first. Number("")is0./catalog?itemid=passes theisNaNcheck, because an empty string converts to0. Validate "present and non-empty" too.- Smart quotes in code.
’and“pasted from slides or docs break HTML attributes and JS strings. res.send(JSON.stringify(x))vsres.json(x). Same text, but onlyres.jsonlabels it as JSON.
Practice problems
These finish the parts of the lecture's "Now You Try" tables that aren't in the in-class server.js yet. Add them to the Week 1 repo and test them in Postman.
Problem 1: Route params + query string together
Implement one route so that:
| Request | Response |
|---|---|
GET /browse/frozen?keywords=organic+corn | Searching for organic corn in the frozen category |
GET /browse/meat?keywords=rotisserie+chicken | Searching for rotisserie chicken in the meat category |
Hint
The category is part of the path, so it's a route param. keywords is after the ?. In a query string, + is decoded as a space, just like %20.
Solution
js
app.get('/browse/:category', (req, res) => {
const { category } = req.params; // "frozen"
const { keywords } = req.query; // "organic corn" ('+' already decoded)
res.send(`Searching for ${keywords} in the ${category} category`);
});Problem 2: Finish the address form
Fix the /getform HTML (quotes and field names) and add the handler it posts to, so that submitting 123 Main St, Smallville, 01234 responds with:
Great! Your package will be shipped to 123 Main St, Smallville, 01234
Hint
The form's name attributes become the keys in req.body. HTML form posts are URL-encoded, not JSON, so which middleware do you need?
Solution
js
app.use(express.urlencoded()); // alongside express.json(), before the routes
app.get('/getform', (req, res) => {
res.send(`<html><body>
<form action="/submitform" method="post">
<label for="address">Address:</label><br />
<input type="text" id="address" name="address" placeholder="123 Main St" /><br />
<label for="city">City:</label><br />
<input type="text" id="city" name="city" placeholder="Smallville" /><br />
<label for="zip">Zipcode:</label><br />
<input type="text" id="zip" name="zip" placeholder="01234" /><br /><br />
<input type="submit" value="Submit" />
</form>
</body></html>`);
});
app.post('/submitform', (req, res) => {
const { address, city, zip } = req.body;
res.send(`Great! Your package will be shipped to ${address}, ${city}, ${zip}`);
});Also note for= instead of htmlFor=: htmlFor is React's spelling. In a plain HTML string the attribute is for.
Problem 3: Validate POST /tracks
Change POST /tracks so that a body missing artist gets 400 with the text artist must be a valid, non-empty string, and the same for title. A valid request should still get the success JSON, sent with res.json.
Solution
js
app.post('/tracks', (req, res) => {
const { artist, title, album, year } = req.body;
for (const [name, value] of [['artist', artist], ['title', title]]) {
if (typeof value !== 'string' || value.trim() === '') {
res.status(400).send(`${name} must be a valid, non-empty string`);
return;
}
}
res.json({
status: 'success',
trackAdded: { artist, title, album, year },
timestamp: new Date().toISOString(),
});
});typeof value !== 'string' catches missing fields (undefined) and wrong types ("artist": 42). The trim() check catches " ".
Problem 4: Comma-separated keywords
One route should handle both:
| Request | Response |
|---|---|
GET /products/electronics/tvs?keywords=lcd,smart,sony | You are looking for [lcd AND smart AND sony] in electronics/tvs, aren't you? |
GET /products/household/kitchen?keywords=timer,analog | You are looking for [timer AND analog] in household/kitchen, aren't you? |
Solution
js
app.get('/products/:department/:category', (req, res) => {
const { department, category } = req.params;
const keywords = String(req.query.keywords ?? '').split(',');
res.send(
`You are looking for [${keywords.join(' AND ')}] in ${department}/${category}, aren't you?`
);
});?? '' (nullish coalescing) supplies a default when keywords is missing, so .split never runs on undefined.
Problem 5: Make /people crash-proof
The lecture's /people route crashes on GET /people (no query string). Make it return 400 letter query parameter is required when letter is missing or empty, and otherwise behave the same.
Solution
js
app.get('/people', (req, res) => {
const { letter } = req.query;
if (typeof letter !== 'string' || letter === '') {
res.status(400).send('letter query parameter is required');
return;
}
const people = ['Alice', 'Bob', 'Charlie', 'Daniela', 'Ella', 'Fred', 'Gaurav'];
const result = people.find((p) => p[0].toLowerCase() === letter[0].toLowerCase());
res.send(result ? result : `No person found for ${letter}`);
});typeof letter !== 'string' also rejects ?letter=a&letter=b, which Express parses as an array.
Review questions
1. You define app.post('/submit', …) and visit http://localhost:3000/submit in your browser. What happens, and why?
Answer
You get Cannot GET /submit (a 404). Typing a URL into the browser sends a GET request, and there's only a POST route for that path. Routes match on method and path. Use a form with method="post" or Postman to send a POST.
2. For each piece of data, would you use a route param, a query string, or the body? (a) the id of the book to fetch (b) a search term and sort order (c) a new user's name, email, and password
Answer
- (a) Route param:
/books/42. It identifies which resource. - (b) Query string:
/books?search=tolkien&sort=year. These are options for a request. - (c) Body: sent with POST. It's the data being created, and it can be large and structured. (It should also never be in a URL, which gets logged and saved in browser history.)
3. Your POST handler does const { firstName } = req.body; and crashes with Cannot destructure property 'firstName' of 'req.body' as it is undefined. What's the most likely cause?
Answer
The body-parsing middleware isn't running for that request. Either app.use(express.json()) is missing, it's registered after the route, or the client sent a different content type (e.g. Postman's body isn't set to raw JSON, or a form was posted without express.urlencoded()). Express only creates req.body when middleware parses it.
4. Why does the /catalog handler need return after res.status(400).send(...)?
Answer
res.send doesn't stop the function. Without return, execution continues to the success res.send(...), which tries to send a second response for the same request. Express then throws Cannot set headers after they are sent to the client. Each request gets exactly one response.
5. What's the difference between a web server serving static content and a web application? Where does your Express code fit?
Answer
A static web server returns files that already exist, the same bytes for everyone. A web application generates responses dynamically, running code per request based on the input (params, query, body, and later a database). Express code is the application tier of a three-tier web app: every handler builds its response at request time.