018 / SI 679 · Fall 2026

The backend
field guide.

Read each lesson with its code, review the short summary, then check your reasoning. Start with ordering pizza.

Weeks 1–6 + TypeScript + HW142 recall questions6 complete original guides
Lesson → In short → check → explain → retry.Find a concept /

Choose an answer before opening the reasoning. Then explain it aloud; try a new example without these notes.

No topics match. Try a shorter term, or clear the search.

00

Section 0 · no coding vocabulary needed

Before the code: order a pizza.

0.1 · Start with something you already know

Imagine ordering pizza on your phone. You see a menu, choose a pizza, tap “Place order,” and get an order number. That is enough to begin; this section needs no code.

Who talks to whom when you orderYou, the person, tap Menu in the phone app. The app is the client, on the frontend: what you see and touch. Step 1: the client sends a request, “send the menu”, to the server, the program on the backend. Step 2: the server sends a response, the menu data. Step 3: the client shows the menu.Youthe persontaps MenuFrontendwhat you seeand touchClientthe phone app3shows the menu1Request“send the menu”2Responsethe menu dataBackendthe work behindthe screenServerthe program that replies
Notice that you never talk to the server yourself: the client asks for you, and only the client draws the screen.

The phone app is the client: the software that asks. Its message is a request. The server is the program that receives it and replies (the word can also mean its computer). The reply is the response.

The frontend is what you see and touch; the backend is the work behind it: receive, check, do the work, reply. This course teaches the backend.

In short

You are the person; the app is the client. It sends a request, the server replies with a response, and the client shows it. Frontend: what you touch. Backend: the work behind it.

CHECK · predict before you choose

The server returns menu data. What happens next?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • The client displays the menu — Receiving data and drawing the menu are separate jobs.
  • The response draws the screen by itself — The reply carries data; client code chooses how to show it.
  • The person becomes the server — The person uses software; the server remains the receiving program.

0.2 · Two kinds of asking

Reading the menu and placing an order are different actions. Each request names its action with a method and its subject with a path.

Reading and creatingTwo requests side by side. Reading the menu: method GET, path /menu, no body. The reply is the menu, as data. Placing an order: method POST, path /orders, and a body with pizza cheese and quantity 1, written as JSON. The reply is an order number, or why the order was rejected.Readopen the menuRequestMethodGETPath/menuBodynoneReplythe menu, as dataCreateplace an orderRequestMethodPOSTPath/ordersBody{"pizza":"cheese","quantity":1}Replyan order number, orwhy it was rejected
Notice that only the request that creates something carries a body, and that a reply can also say no.
  • An API is the agreed way to ask: which requests the server supports, what to send, and what each reply means.
  • HTTP is the format every web request and response follows.
  • JSON is a common way to write structured data. The order’s body is two labeled facts: pizza is cheese, quantity is one.
  • REST is a set of conventions for designing an API around resources, such as menus and orders.

Follow one order ticket

Step through a successful order, then compare a rejected one. This is a local teaching simulation.

  1. Client → checks → controller → service → storage → reply

A rejected request stops before the order is saved. The client displays the reply.

In short

GET /menu reads; POST /orders creates, with the order as a JSON body. The API is the agreed set of such requests. HTTP carries each message; JSON structures its data.

CHECK · predict before you choose

A library app creates a reservation. Which request fits this design?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • GET /reservations to save a new reservation — GET is for reading in this design; it should not create reservations.
  • JSON /reservations — JSON is a data format, not an HTTP method.
  • POST /reservations with book details — Creating a resource uses POST here, with the needed data in its body.

0.3 · Open the kitchen door

Inside the server, roles share the work like kitchen stations. They are parts of one program, not people or separate computers.

Inside the serverInside the server, a request passes down five roles: route picks the station, middleware checks the ticket, controller builds the reply, service applies the rules, database keeps the order book. Express runs the first three, MongoDB is the database, Node.js runs everything. A failed check replies at once. TypeScript checks code while you write; tests rehearse requests.a failed check repliesrequestServerRoutepicks the stationMiddlewarechecks the ticketControllerbuilds the replyServiceapplies the rulesDatabasekeeps the order bookExpress handlesthese threeMongoDBNode.js runs the whole JavaScript programTypeScript checks codewhile you write it,not incoming orders.Tests rehearse:send a known order,check the reply.
Notice that a failed check replies before the order reaches the service or the database.
  • Route: picks the code that handles the request.
  • Middleware: checks the request on the way in. Are the details usable (validation)? Who is asking (authentication)? May they do this (authorization)? A failed check replies at once.
  • Controller: reads the request, asks for the work, builds the response.
  • Service: applies the rules, such as which pizzas are available.
  • Database: keeps records that survive a restart, like a saved order book.
  • Model and document: a model is the shape of an order, like a blank form; a document is one saved order in MongoDB.

The course tools:

  • JavaScript: the language the code is written in.
  • TypeScript: checks value types while you write. It never checks an incoming order, so validation still matters.
  • Node.js: runs JavaScript outside the browser; here, the whole server.
  • Express: receives requests, runs routes and middleware, sends responses.
  • MongoDB: the database that stores the orders.
  • Tests: rehearsals. Send a valid order, expect it accepted; send quantity −2, expect it rejected.

In short

Route directs, middleware checks, controller handles HTTP, service applies rules, database stores. Node runs it all, Express handles the web parts, MongoDB stores, tests rehearse.

CHECK · predict before you choose

An order contains quantity −2. What should happen before saving it?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • Save it because a model says quantity is a number — A numeric type does not enforce a positive-value rule.
  • Ask the database to draw an error message — The backend replies with an error; the client presents it.
  • Validate and reject the unusable quantity — Runtime validation must stop invalid orders before accepting them.

0.4 · Say it in your own words

Pause before opening each answer. A rough explanation is enough.

You tap “Place order.” Are you the client? What is the request?

You are the person; the app is the client. The request is the one message asking the server to create the order. The API is the agreed way to send it.

The server sends menu data. Who makes it appear on screen? Must every menu request use a database?

The client. A database is one possible source; the server could also read a file or memory. The response is data, not the screen.

An order says quantity is −2. Which job should catch that, and what happens next?

Validation, before the order is accepted. Middleware can reject it with an error response, which the client shows. TypeScript cannot check an incoming message.

The same pattern fits any app:

  • Weather: pick Ann Arbor → the client asks for its forecast → the server replies with data → the client draws rain icons.
  • Library: tap “Reserve” → the client sends a reservation request → the server checks the rules and records it → the client shows the reply.

For any example, ask three questions: What does the person want? What message does the client send? What must the server do before it can reply?

New to the terminal? Get your computer ready first.

Continue to Section 1: see how this conversation is written in code. Come back here whenever a new term feels disconnected.

In short

Connect the jobs without notes: person → client → request → server → response → client display. Then explain a failure: unusable input should be rejected before the requested work is accepted.

CHECK · predict before you choose

A shop API replies “order rejected.” Which statement is supported?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • No request was sent — An error reply can be produced in response to an actual request.
  • The server must have saved the order — Rejection does not imply a successful write; inspect the contract and state.
  • A response can report a failure — Replies communicate outcomes, including rejected requests.

Source trail · New everyday teaching examples connecting the retained Week 1–4 and HW1 topics; illustrative API paths, not coursework endpoints.

0+

Before Week 1 · terminal, Node, an editor, git, and a first server

Get your computer ready.

Open a terminal and move around

Connection to Section 0: the pizza story showed a server receiving requests. This section gets your own computer ready to run one.

A terminal is a window where you type a command, press Enter, and read the text the computer prints back. The tools in this course are started by typed commands, so everything begins here.

  • macOS: press Cmd+Space, type Terminal, and press Enter.
  • Windows: open the Start menu, type PowerShell, and press Enter.

The terminal shows a prompt: a short line that means it is waiting for you. In this guide, a line that starts with $ is a command to type, without the $. Every other line is what the terminal prints.

macOS Terminal · example output, your folders will differ
$ pwd
/Users/you
$ ls
Desktop     Documents   Downloads
$ mkdir si679
$ cd si679
$ pwd
/Users/you/si679
  • pwd prints the folder you are in right now.
  • ls lists what is inside that folder.
  • mkdir si679 makes a new folder named si679.
  • cd si679 moves you into that folder. cd .. moves you back up one level.

A path is the address of a folder or file: the folders you pass through to reach it, in order. /Users/you/si679 reads as the Users folder, then your own folder, then si679. The symbol ~ is short for your own folder, so ~/si679 is the same place.

Windows writes a path with a drive letter and backslashes. The same four commands work in PowerShell, where pwd and mkdir print a small table.

Windows PowerShell · example output, your date will differ
PS C:\Users\you> pwd

Path
----
C:\Users\you


PS C:\Users\you> mkdir si679


    Directory: C:\Users\you


Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----          9/1/2026   9:14 AM                si679


PS C:\Users\you> cd si679
PS C:\Users\you\si679>

Most setup mistakes are a correct command typed in the wrong folder. When something fails, pwd is the first command to run.

In short

A terminal runs typed commands. pwd shows where you are, ls shows what is there, mkdir makes a folder, and cd moves you. A path is a folder’s address.

CHECK · predict before you choose

You run mkdir si679, then pwd. The path does not end in si679. Why?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • mkdir failed without saying so — mkdir prints an error when it fails. No error means the folder exists.
  • mkdir made the folder but did not move you into it — Making a folder and entering it are separate steps. Run cd si679 to move.
  • pwd always prints your home folder — pwd prints the folder you are in now. It changes after cd.

Install Node and check it

Node runs JavaScript on your computer, outside a browser. In this course it runs your server program.

npm is installed together with Node. It downloads packages, which are pieces of code other people wrote, and it runs the scripts a project defines.

  1. Download the LTS installer from nodejs.org ↗ and run it. LTS means long-term support: the release meant for everyday use.
  2. Close the terminal and open a new one, so it can find the new programs.
  3. Ask both programs for their version.
macOS Terminal or Windows PowerShell · example output, your numbers may differ
$ node --version
v24.21.0
$ npm --version
11.17.0

A version number from each command means the install worked. The commands and their output are the same on macOS and Windows.

The Week 3 course notes say their code was tested with Node 24.21.0, that a version close to it (22 or newer) will likely work, and that a version below 20 should be upgraded. The notes do not name an npm version. The npm number above is one example from a Node 24 install.

If the terminal answers “command not found”, see the last topic in this section.

In short

Node runs JavaScript outside the browser. npm downloads packages and runs project scripts. A version number from both commands means they are installed.

CHECK · predict before you choose

node --version prints a number starting with v18. What do the course notes suggest?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • Upgrade Node, because the notes say a version below 20 should be upgraded — The notes tested with Node 24 and call a version below 20 old enough to upgrade.
  • Nothing, because any version number means the code will run — A number proves Node is installed. It does not prove the version is recent enough.
  • Reinstall npm, because npm decides the Node version — npm comes with Node, not the other way around. Install a newer Node.

Open your project in a code editor

A code editor is a program for writing plain text files such as server.js. A word processor is the wrong tool: it saves hidden formatting that code cannot contain.

This guide uses Visual Studio Code, called VS Code, a free editor from Microsoft. Any code editor works. Download it from code.visualstudio.com ↗ and install it.

  1. Open VS Code.
  2. Choose File, then Open Folder.
  3. Select your si679 folder and confirm.

Open the folder, not a single file. The left panel then lists every file in the project, and a new file you create is saved inside it.

VS Code has a terminal built in: choose Terminal, then New Terminal. It starts inside the folder you opened, so cd is not needed.

You can also open a folder from the terminal. The dot means the folder you are in.

Shell · prints nothing, a VS Code window opens
$ cd ~/si679
$ code .

On Windows the installer normally sets up the code command. On macOS, set it up once: in VS Code press Cmd+Shift+P, type shell command, and choose the entry that installs it.

In short

A code editor writes plain text files. Open the whole project folder, not one file. The built-in terminal starts inside that folder.

CHECK · predict before you choose

You want VS Code to list every project file and start its terminal inside the project. What do you open?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • The file server.js by itself — One file opens without its project. The file list stays empty.
  • A word processor document named server.js — A word processor saves formatting that is not plain text. Node needs plain text.
  • The project folder, with File then Open Folder — Opening the folder gives the editor and its terminal the whole project.

Save and hand in work with git and GitHub

git is a program that records versions of a project folder. A folder that git tracks is called a repository.

A commit is one saved version, with a short message that says what changed. Commits are stored on your computer.

GitHub is a website that keeps a copy of a repository online. To clone is to download a repository from GitHub to your computer. To push is to send your new commits up to GitHub.

Shell · example output from one Mac
$ git --version
git version 2.50.1 (Apple Git-155)
  • macOS: the first time you run git, the system offers to install its developer tools. Accept, then run the command again.
  • Windows: install Git for Windows from git-scm.com ↗, then open a new terminal.

Create a free account on github.com ↗ before the first assignment.

The course hands out work through Classroom50. The Week 1 page describes the flow: accept the assignment, clone the resulting repository, do the work, and push it. The Week 3 notes add npm install as a step after cloning.

  1. Open the assignment link and accept it. GitHub creates a repository for you.
  2. On that repository’s page, press the Code button and copy the URL.
  3. Clone it, move into the new folder, and install its packages. Paste your own URL in place of the example one.
  4. Do the work in your editor and save the files.
  5. Stage the files you changed, commit them, and push.
Shell · step 3 · shortened example output, your names and numbers will differ
$ cd ~/si679
$ git clone https://github.com/ORGANIZATION/si-679-f-26-week1-nyt-yourname.git
Cloning into 'si-679-f-26-week1-nyt-yourname'...
$ cd si-679-f-26-week1-nyt-yourname
$ npm install

added 68 packages, and audited 69 packages in 3s
Shell · step 5 · shortened example output
$ git add server.js
$ git commit -m "Add catalog route"
[main ee6030f] Add catalog route
 1 file changed, 3 insertions(+)
$ git push
To https://github.com/ORGANIZATION/si-679-f-26-week1-nyt-yourname.git
   e317026..ee6030f  main -> main
  • git add server.js stages that file: it marks the change to go into the next commit. Name each file you changed.
  • git commit -m "..." saves the staged changes as one commit with your message.
  • git push sends your commits to GitHub. Until you push, the work exists only on your computer.
  • git status lists what changed and what is staged. Run it whenever you are unsure.

On a new computer the first commit may stop with “Please tell me who you are”. Run the two git config commands it prints, with your own name and email, then commit again. The first push may ask you to sign in to GitHub.

In short

A repository is a folder git tracks. Clone downloads it, commit saves a version on your computer, and push sends commits to GitHub. Classroom50 flow: accept, clone, npm install, work, add, commit, push.

CHECK · predict before you choose

You ran git commit. The repository page on GitHub still shows the starter code. Why?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • GitHub needs a day to show new commits — GitHub shows a commit as soon as it is pushed. Nothing has been sent yet.
  • git commit erased the changes — A commit saves changes in the repository on your computer. It erases nothing.
  • The commit stays on your computer until you run git push — Commit records the version on your computer. Push sends it to GitHub.

Create a project and install Express

These are the first steps on the Week 1 course page. A project is a folder with a package.json file in it.

Shell · example output from npm 11, your path and counts will differ
$ cd ~/si679
$ mkdir week1hello
$ cd week1hello
$ npm init -y
Wrote to /Users/you/si679/week1hello/package.json:

{
  "name": "week1hello",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "type": "commonjs"
}

$ npm install express@latest

added 68 packages, and audited 69 packages in 3s

28 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
$ ls
node_modules        package-lock.json   package.json
  • npm init -y creates package.json, the file that describes the project: its name, its scripts, and the packages it needs. The -y accepts every default answer.
  • npm install express@latest downloads Express and the packages Express itself needs. @latest asks for the newest release.
  • node_modules is the folder where npm puts downloaded packages. Never edit it.
  • package-lock.json records the exact version of every installed package.

Express is the library that helps a Node program receive HTTP requests. Installing it does not start anything yet.

Now open package.json in your editor and make the three changes the Week 1 page makes.

  • "type": "module" turns on import, the way your files load other code.
  • "main": "server.js" names the file that starts the program.
  • The start script tells npm start what to run. node --watch restarts the program each time you save a file.
package.json · after the three edits
{
  "name": "week1hello",
  "version": "1.0.0",
  "description": "",
  "main": "server.js",
  "scripts": {
    "start": "node --watch server.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "type": "module",
  "dependencies": {
    "express": "^5.2.1"
  }
}

JSON is strict. Keep the double quotes, put a comma between entries, and put no comma after the last one. The dependencies entry was added by npm install. The Week 1 page shows the same Express version range.

In short

npm init -y creates package.json. npm install puts packages in node_modules and lists them under dependencies. The type line turns on import, and the start script gives npm start something to run.

CHECK · predict before you choose

Someone deletes node_modules by accident. package.json is untouched. What brings the packages back?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • Run npm install in the project folder — npm reads the dependencies listed in package.json and downloads them again.
  • Run npm init -y again — npm init creates package.json. It downloads no packages.
  • Type the package files again by hand — node_modules is generated. npm rebuilds it from the list.

Run the server and visit it

In the week1hello folder, create a file named server.js with the Hello World route from the Week 1 page.

JavaScript · server.js
import express from 'express';

const app = express();
const port = 3000;

app.get('/', (req, res) => {
  res.send('Hello World!');
});

app.listen(port);

Section 1 reads this code line by line. For now, know that the last line starts a program that waits for requests on port 3000.

If this is your first JavaScript, the JavaScript section after setup teaches the language one piece at a time.

Shell · example output
$ npm start

> week1hello@1.0.0 start
> node --watch server.js

The terminal prints the script it runs, then waits. No new prompt appears. That is correct: the server is running, and this terminal is busy until you stop it. The Week 1 page starts the same file with node ./server.js, which prints nothing at all.

Go to http://localhost:3000 in a browser.

Browser · the page shows
Hello World!
  • localhost is a name for this computer. The request never leaves your machine.
  • A port is a numbered door on a computer. A program listens on a port, and only one program can use a port at a time. This server uses 3000, so :3000 ends the address.

To stop the server, click its terminal and press Ctrl+C. Use the Ctrl key on a Mac too. The prompt returns, and the browser can no longer load the page.

If you start the server while an earlier copy is still running, Node stops with this error.

Shell · the line that matters, from a longer error
Error: listen EADDRINUSE: address already in use :::3000

It means port 3000 is taken, usually by your own server in another terminal window. Find that terminal, press Ctrl+C, and start again.

In short

npm start runs the server, and the terminal waits. localhost means this computer, and the port is the door the server listens on. Ctrl+C stops it. EADDRINUSE means the port is already taken.

CHECK · predict before you choose

npm start printed two lines, and now the terminal just sits there. What is happening?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • The server crashed without a message — A crash prints an error. A quiet wait is the normal running state.
  • The server is running and waiting for requests — A server’s job is to wait. Open http://localhost:3000 to send it a request.
  • npm is still downloading Express — npm start runs a script. Downloading happens during npm install.

Send requests with Postman

Postman is an app that sends an HTTP request you build by hand and shows the whole response: the status, the headers, and the body. Download it from postman.com/downloads ↗ and install it.

You need it because the browser address bar can only send GET. Section 1 explains why a POST route needs a different client.

The Week 1 page sets Postman up like this.

  1. Create a collection named SI 679 and, inside it, a folder named Week1. A collection is a saved group of requests.
  2. Add a request named hello. Set the method to GET. Use http://localhost:3000/ as the URL.
  3. Start your server with npm start, then press Send.
Postman · response
200 OK

Hello World!

200 OK is the status: the server’s short code for success. The body under it is the same text the browser showed.

If Postman cannot connect, the server is not running or the port in the URL is wrong.

Later in Section 1, the same steps send POST with a JSON body.

In short

Postman sends any HTTP method and shows the status, headers and body. The address bar only sends GET. Start the server before you press Send.

CHECK · predict before you choose

You need to try a route registered with app.post. Which tool fits?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • Postman, with the method set to POST — Postman lets you choose the method and add a body.
  • The browser address bar, with the same URL — The address bar sends GET, so the POST route never runs.
  • The terminal command npm install — npm install downloads packages. It sends no request to your server.

Before Week 3: install MongoDB

Weeks 1 and 2 do not need this topic. Week 3 uses a database from the start of lecture, and the course prep page asks you to have it working before class.

MongoDB is a database: a program that stores data and keeps it after your server stops. The prep page lists three things to install.

  • MongoDB Community Edition is the database server. mongod is the name of its program.
  • mongosh is the Mongo shell, a command-line tool for talking to that server.
  • MongoDB Compass is an app with windows and buttons for looking at the same data.
  1. Follow MongoDB’s installation instructions for your platform ↗. When the prep page was captured, it said the current release was 8.3 and that installing the latest is fine.
  2. Run mongosh --version. If the command is not found, install the shell separately: mongosh instructions ↗. Some platforms do not include it with the server.
  3. Install Compass for your platform ↗

Nothing can connect until the server program mongod is running. The platform instructions either set it up as a background service that starts with your computer, or have you start it by hand. The prep page prefers the background service where it is offered.

If you start it by hand, or it will not stay running, give it a data folder. Any folder you can write to works.

macOS or Linux
$ mkdir -p ~/data/mdata
$ mongod --dbpath ~/data/mdata
Windows PowerShell
PS C:\Users\you> mkdir c:\data\mdata
PS C:\Users\you> mongod --dbpath c:\data\mdata

mongod prints many log lines and keeps running, the way your Express server did. Leave that terminal open. It listens on port 27017, the MongoDB default.

Open a second terminal and connect with the shell. It prints a few lines about the connection, then a prompt.

Shell · adapted from the Week 3 notes, your sizes will differ
$ mongosh
test> show databases
admin     40.00 KiB
config    72.00 KiB
local     40.00 KiB

A prompt like test> means the shell reached the server. The three databases are admin, config and local. MongoDB creates them itself, so seeing them means a real server answered. Type exit to leave the shell.

Shell · the server is not running
MongoNetworkError: connect ECONNREFUSED 127.0.0.1:27017

That error means nothing is listening on the port. Check the terminal where you started mongod, or the status of the background service.

Then run the same check in Compass.

  1. Open Compass and click Add new connection.
  2. The URI field should read mongodb://localhost:27017. Set it if it does not. A URI is the address of the server: the kind of connection, the computer, and the port.
  3. Click Connect, then expand the connection in the left sidebar.

You are done when mongosh gives you a prompt and Compass shows admin, config and local. The Week 3 code writes the same address as mongodb://127.0.0.1:27017, because 127.0.0.1 is the numeric form of localhost.

In short

mongod is the database server and must be running. mongosh and Compass are two ways to look inside it. The default port is 27017. A mongosh prompt and the databases admin, config and local mean it works.

CHECK · predict before you choose

mongosh stops with MongoNetworkError: connect ECONNREFUSED. What do you check first?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • Whether Compass is installed — Compass is a separate viewer. mongosh connects without it.
  • Whether mongod is running — ECONNREFUSED means nothing is listening on the port. Start the server, then try again.
  • Whether the admin database was deleted — The error comes before any database is read. No server answered.

When setup goes wrong

Read the first line of an error before anything else. It usually names the problem. These five cover most setup failures.

Shell · real messages, paths changed to the example folders
zsh: command not found: node

npm error enoent Could not read package.json: Error: ENOENT: no such file or directory, open '/Users/you/si679/package.json'

Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'express' imported from /Users/you/si679/week1hello/server.js

Error: listen EADDRINUSE: address already in use :::3000
  • command not found: the terminal cannot find that program. It is not installed, or this terminal was opened before the install finished. Open a new terminal and try again. PowerShell words the same problem as “is not recognized”.
  • Could not read package.json: you are in the wrong folder. Check with pwd, then use cd to enter the project folder.
  • Cannot find package: the packages are not downloaded. A cloned starter needs npm install once before it can run.
  • The browser cannot connect: no program is listening. Start the server with npm start, keep its terminal open, and check the port in the URL.
  • address already in use: another program holds the port, usually an earlier copy of your server. Press Ctrl+C in that terminal, then start again.

The desk reference has a longer table for later weeks.

In short

Five causes cover most setup failures: program not installed, wrong folder, packages not installed, server not running, port already taken. Read the first error line, then check where you are with pwd.

CHECK · predict before you choose

You clone a starter repository and run npm start right away. It stops with Cannot find package 'express'. What fixes it?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • Install Node again — Node ran and reported the error, so Node works. A package is missing.
  • Change the port number — The program stopped before it could listen. The port is not involved.
  • Run npm install in that folder, then start again — A cloned starter lists its packages but does not contain them. npm install downloads them.

Source trail · Week 1 course page (Express project, Postman, Now You Try flow), the Node version in the Week 3 notes, and the Week 3 prep page. Terminal, editor and git basics are general usage. Example output was captured on one Mac.

JS

Before Week 1 · values, objects, functions, callbacks, modules, await, and errors

Learn the language first.

If you know Python, start here

Most of JavaScript has a Python twin. Each row below pairs the two and names the trap, and every topic after this one shows its example in both languages, side by side. New to both? Start with Run one line of JavaScript instead.

PythonJavaScriptWatch out
print('Order received')console.log('Order received');Node prints an object in its own format, not as JSON.
x = 1const x = 1;
let x = 1;
Make a name once, with const or let. A const name cannot take a new value.
items = ['a', 'b']const items = ['a', 'b'];Called an array. Its push is Python’s append. Reading past the end, as items[5] does, gives undefined instead of an error.
order = {'pizza': 'cheese'}const order = { pizza: 'cheese' };Called an object. Keys are strings, written without quotes and read with order.pizza. A missing key gives undefined instead of an error.
def total(price, quantity):
lambda price: price * 2
function total(price, quantity) { }
(price) => price * 2
Without return, the result is undefined, not None, and the arrow form is the one course code uses most.
for item in items:for (const item of items) { }Write of, not in: a loop written with for...in gives the indexes, as strings.
if / elif / else
and, or, not
if / else if / else
&&, ||, !
Parentheses around the condition, braces around each block. An empty array is truthy.
=====Use three. Two equals signs convert types first, so '5' == 5 is true.
f'{quantity} pizzas'`${quantity} pizzas`Backticks, not quotes, and a dollar sign before the braces.
Nonenull
undefined
undefined has no Python twin. A missing field, a missing argument and a function without return all give it.
try:
except ValueError as error:
try { }
catch (error) { }
Python’s raise is throw. One catch takes every error, and error.message holds its text.
from menu import menuimport { menu } from './menu.js';The quoted part is a path, with ./ and .js. Only names marked export can be imported.
async def
await
async function
await
The same idea. Node allows await at the top of a module, where Python needs asyncio.run to start.
len(items)items.lengthA property, so no parentheses.
'a' + 1 # TypeError'a' + 1 // 'a1'JavaScript joins instead of failing. Convert first: Number('5') is 5, like int('5') in Python.
  • Braces mark a block, not indentation. Indent anyway, so people can read it.
  • Semicolons end statements. JavaScript can usually do without them, but the course writes them, and so does this guide.
  • Make every name with const. Switch to let only when the name must take a new value.
  • undefined is a real value, not an error. You will meet it in bugs: a misspelled field, a missing await, or a function with no return statement.

In short

Most Python has a JavaScript twin: print is console.log, a dict is an object, a list is an array, an f-string is a template literal. Watch for === instead of ==, braces instead of indentation, const, and undefined, which Python does not have.

CHECK · predict before you choose

JavaScript
const quantity = '2';
console.log(quantity + 1);
What does this file print when Node runs it?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • 3 — quantity holds the string 2, not the number. With a string on one side, + joins text.
  • 21 — The string 2 and the number 1 are joined as text. Python would stop here with a TypeError.
  • A TypeError, as in Python — Python refuses to add a string and a number. JavaScript turns the 1 into text and joins them, with no error.

Run one line of JavaScript

Connection to setup: Node is installed and your editor can open a folder. This section teaches the language Node runs, one piece at a time.

JavaScript is a language for writing instructions a computer follows. A program is a file of those instructions. Each topic here adds one piece of the course code.

Make a practice folder first. Inside your si679 folder, create a folder named js-primer and open it in your editor, as in setup. Then create a file named hello.js with these four lines and save it.

JavaScript · hello.js
// A line that starts with // is a comment. Node skips it.
console.log('Order received');
console.log(2 + 3);
console.log('Done');

In the terminal, move into js-primer and ask Node to run the file.

Shell · output
$ node hello.js
Order received
5
Done

Python

# A line that starts with # is a comment. Python skips it.
print('Order received')  # → Order received
print(2 + 3)  # → 5
print('Done')  # → Done

JavaScript

// A line that starts with // is a comment. Node skips it.
console.log('Order received'); // → Order received
console.log(2 + 3); // → 5
console.log('Done'); // → Done
  • Each line that ends with ; is a statement: one complete instruction.
  • Node runs the statements from top to bottom, one at a time.
  • console.log(...) prints whatever is inside the parentheses to the terminal. A program prints nothing unless a statement tells it to.
  • Text goes inside quotes. 2 + 3 has no quotes, so Node works it out and prints the result.

If Node answers Cannot find module, it could not find the file. Check the spelling of the name. Then check with pwd and ls that the terminal is in the folder that holds it.

In short

A program is a file of statements. node hello.js runs them from top to bottom. console.log prints a value to the terminal. A line that starts with // is a comment and does not run.

CHECK · predict before you choose

JavaScript
console.log('Take order');
// console.log('Bake pizza');
console.log(4 + 4);
What does this file print when Node runs it?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • Take order, then Bake pizza, then 8 — The second line starts with //, so it is a comment. Node skips it.
  • Take order, then 8 — The comment is skipped, and 4 + 4 has no quotes, so Node works it out.
  • Take order, then 4 + 4 — Only text inside quotes is printed as written. Without quotes, Node adds the numbers.

Values: text, numbers, and true or false

A value is one piece of data. Every value has a type, which is the kind of data it is. The course uses five kinds all the time.

  • A string is text. 'cheese' is one: the quotes mark where the text starts and ends. Single quotes and double quotes both work, as long as the string starts and ends with the same one.
  • A number has no quotes. 2 and 5.99 are numbers, and the signs +, -, * and / add, subtract, multiply and divide them.
  • true and false are the two booleans. A boolean answers a yes-or-no question.
  • undefined means no value was ever given.
  • null means someone deliberately wrote “nothing here”.

typeof reports the type of a value. Save these lines as values.js and run the file.

JavaScript · values.js
console.log(typeof 'cheese');
console.log(typeof 2);
console.log(typeof true);
console.log(typeof undefined);

console.log(5 + 5);
console.log('5' + '5');
console.log('5' + 5);
console.log(Number('5') + 5);
Shell · output
$ node values.js
string
number
boolean
undefined
10
55
55
10

Python

print(5 + 5)  # → 10
print('5' + '5')  # → 55
print('5' + 5)  # → TypeError: can only concatenate str (not "int") to str

JavaScript

console.log(5 + 5); // → 10
console.log('5' + '5'); // → 55
console.log('5' + 5); // → 55

The last four lines are why the type matters. With two numbers, + adds. With a string on either side, + joins text instead, so '5' + 5 is the text 55. Python refuses that line with an error instead.

This comes up in Week 1. Everything a client puts in a web address arrives as a string, even when it looks like a number. Number('5') converts the string to a number, and then + adds.

In short

A string is text in quotes, a number has no quotes, and a boolean is true or false. undefined means never given; null means deliberately empty. The string 5 and the number 5 are different values: + joins strings and adds numbers.

CHECK · predict before you choose

JavaScript
console.log('2' + 3);
console.log(2 + 3);
What does this file print when Node runs it?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • 23, then 5 — A string on one side makes + join text. Two numbers are added.
  • 5, then 5 — The quotes matter. The first 2 is a string, and + joins a string with whatever is beside it.
  • An error, then 5 — JavaScript does not refuse to mix a string and a number. It quietly joins them, which is why this mistake is easy to miss.

Names for values: const and let

A variable is a name that holds a value, so later lines can use the value by name. const and let each create one.

JavaScript · names.js
const pizza = 'cheese';
let quantity = 1;

quantity = quantity + 1;

console.log(pizza);
console.log(quantity);
Shell · output
$ node names.js
cheese
2

Python

pizza = 'cheese'
quantity = 1

quantity = quantity + 1

print(pizza)  # → cheese
print(quantity)  # → 2

JavaScript

const pizza = 'cheese';
let quantity = 1;

quantity = quantity + 1;

console.log(pizza); // → cheese
console.log(quantity); // → 2
  • const pizza = 'cheese'; creates the name pizza and stores the string in it.
  • A single = does not mean “is equal to”. It means: work out the right side, then store the result under the name on the left. That is how quantity = quantity + 1 makes sense.
  • A name made with const keeps its value. A name made with let can be given a new value later. Python has neither word: any name can change.

Giving a const name a new value stops the program. Put pizza = 'veggie'; on the line after const pizza = 'cheese'; and Node prints this.

Shell · the line that matters, from a longer error
TypeError: Assignment to constant variable.

The course uses const almost everywhere, because a name that cannot change is easier to follow. It uses let only when a name gets its value later or gets a new one: the database connection in Week 3, and a book list that is replaced after a delete in Week 2.

In short

const and let give a value a name. A single = stores the right side under the name on the left. A const name keeps its value; a let name can be given a new one. Use const unless the value must change.

CHECK · predict before you choose

JavaScript
let quantity = 2;
const price = 10;
quantity = quantity + 1;
console.log(quantity * price);
What does this file print when Node runs it?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • 20 — The third line stores a new value in quantity, so it is 3 when the last line runs.
  • An error, because quantity changed — quantity was made with let, so it may change. Only a const name refuses a new value.
  • 30 — quantity becomes 3, and 3 * 10 is 30.

Objects: labeled facts

Section 0 read an order as two labeled facts: pizza is cheese, quantity is one. An object is how JavaScript keeps labeled facts together. Each label with its value is called a property. This guide also calls it a field.

JavaScript · objects.js
const order = { pizza: 'cheese', quantity: 1 };

console.log(order.pizza);
console.log(order.size);

order.quantity = 2;
console.log(order);
console.log(JSON.stringify(order));
Shell · output
$ node objects.js
cheese
undefined
{ pizza: 'cheese', quantity: 2 }
{"pizza":"cheese","quantity":2}

Python

import json
order = {'pizza': 'cheese', 'quantity': 1}

print(order['pizza'])  # → cheese
print(order.get('size'))  # → None

order['quantity'] = 2
print(json.dumps(order))  # → {"pizza": "cheese", "quantity": 2}

JavaScript

const order = { pizza: 'cheese', quantity: 1 };

console.log(order.pizza); // → cheese
console.log(order.size); // → undefined

order.quantity = 2;
console.log(JSON.stringify(order)); // → {"pizza":"cheese","quantity":2}

Python reads a missing key with brackets and stops with a KeyError. JavaScript gives undefined and carries on.

  • order.pizza reads one field: the name of the object, a dot, then the name of the field.
  • Reading a field that is not there gives undefined. It is not an error, so a misspelled field name fails quietly.
  • order.quantity = 2 stores a new value in a field. This works on a const object. const stops the name order from being pointed at a different object. It does not stop the fields from changing.

The last two lines print the same object in two forms. The first is how Node displays an object. The second is JSON: text with stricter rules, such as double quotes around every name, so that it can travel inside a message. JSON.stringify turns an object into JSON text. An object lives in a running program. JSON is text in a request, a response or a file.

A value inside an object can be another object, and the dots chain. order.customer.name reads customer first, then the name field inside it.

In short

An object groups labeled values. A dot reads a field, and a missing field is undefined, not an error. const fixes the name, not the fields. JSON is the text form of the same idea, used in messages.

CHECK · predict before you choose

JavaScript
const book = { title: 'Dune', year: 1965 };
console.log(book.author);
What does this file print when Node runs it?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • undefined — book has no author field, and reading a missing field gives undefined.
  • An error, because author does not exist — Reading a missing field is not an error. That is why a misspelled field name fails quietly.
  • null — null is a value someone writes on purpose. A field that was never written is undefined.

Arrays: an ordered list

An array is a list of values in order. Square brackets start and end it, with commas between the items.

JavaScript · arrays.js
const menu = ['cheese', 'pepperoni', 'veggie'];

console.log(menu[0]);
console.log(menu.length);
console.log(menu[3]);

menu.push('hawaiian');
console.log(menu.length);
console.log(menu);

const books = [
  { title: 'Dune', year: 1965 },
  { title: 'The Hobbit', year: 1937 }
];
console.log(books[1].title);
Shell · output
$ node arrays.js
cheese
3
undefined
4
[ 'cheese', 'pepperoni', 'veggie', 'hawaiian' ]
The Hobbit

Python

menu = ['cheese', 'pepperoni', 'veggie']

print(menu[0])  # → cheese
print(len(menu))  # → 3
print(menu[3])  # → IndexError: list index out of range

JavaScript

const menu = ['cheese', 'pepperoni', 'veggie'];

console.log(menu[0]); // → cheese
console.log(menu.length); // → 3
console.log(menu[3]); // → undefined
  • Each item has a position called an index, and the first index is 0. menu[0] is the first item.
  • .length is how many items there are. The last item sits at index length - 1, so in a list of three, menu[3] is past the end and gives undefined, not an error.
  • push adds an item to the end. Like the fields of an object, the items of a const array can change.
  • An array can hold objects. books[1].title reads the second book, then its title field.

Most data in this course has that last shape: a list of books, menu items or products, each one an object.

In short

An array is an ordered list. Indexes start at 0, .length counts the items, and push adds one to the end. Course data is usually an array of objects.

CHECK · predict before you choose

JavaScript
const sizes = ['small', 'medium', 'large'];
sizes.push('party');
console.log(sizes[1]);
console.log(sizes.length);
What does this file print when Node runs it?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • small, then 4 — Indexes start at 0, so index 1 is the second item.
  • medium, then 4 — Index 1 is the second item, and push made the list four items long.
  • medium, then 3 — push changes the array even though the name was made with const. const only fixes which array the name points at.

Decisions: if, else, and comparisons

A comparison asks a question and produces a boolean.

  • === is true when both sides are the same value of the same type. !== is its opposite.
  • <, >, <= and >= compare sizes.

if runs the statements in its braces only when the condition in its parentheses is true. else runs its own braces when the condition is not true. The statements between one pair of braces are called a block.

JavaScript · decisions.js
const order = { pizza: 'cheese', quantity: 0 };

if (order.quantity === 0) {
  console.log('Quantity is zero');
} else {
  console.log('Quantity is not zero');
}

if (!order.quantity) {
  console.log('Looks missing, but it is 0');
}

if (order.size === undefined || order.pizza === '') {
  console.log('Something is missing');
}

console.log(order.quantity < 1 && order.pizza !== 'veggie');
console.log('5' === 5);
Shell · output
$ node decisions.js
Quantity is zero
Looks missing, but it is 0
Something is missing
true
false

Python

order = {'pizza': 'cheese', 'quantity': 0}

if not order['quantity']:
    print('Looks missing, but it is 0')  # → Looks missing, but it is 0

print(order['quantity'] < 1 and order['pizza'] != 'veggie')  # → True
print('5' == 5)  # → False

JavaScript

const order = { pizza: 'cheese', quantity: 0 };

if (!order.quantity) {
  console.log('Looks missing, but it is 0'); // → Looks missing, but it is 0
}

console.log(order.quantity < 1 && order.pizza !== 'veggie'); // → true
console.log('5' === 5); // → false

&& || ! are Python’s and or not. One trap: an empty array is truthy, though an empty Python list is falsy.

Look at the second if. Its condition is not a comparison, only a value. JavaScript then treats the value as true or false. These values count as false and are called falsy: false, 0, '' (a string with nothing in it), null, undefined and NaN (what a failed number conversion gives). Every other value counts as true and is called truthy. Even the string '0' is truthy.

So !order.quantity is true for a missing quantity and also for a quantity of 0. The course trips on this more than once, because a price of 0 or an empty string can be a real answer. When you mean “missing”, compare with undefined directly, as the third if does.

The last line shows why the course writes three equals signs. '5' === 5 is false, because a string is never the same value as a number.

In short

=== compares value and type. if runs its block when the condition is true; else runs when it is not. false, 0, an empty string, null, undefined and NaN are falsy, so !value cannot tell 0 from missing.

CHECK · predict before you choose

JavaScript
const price = 0;
if (!price) {
  console.log('Price is required');
} else {
  console.log('Price accepted');
}
What does this file print when Node runs it?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • Price accepted — 0 is falsy, so !price is true and the first block runs. A free product would be turned away.
  • Nothing prints — With if and else, exactly one of the two blocks always runs.
  • Price is required — 0 is falsy, so !price is true. This is how a check meant for a missing price also rejects a price of 0.

Loops: repeat for each item

A loop runs the same statements more than once. The course uses two kinds, and only in a few places.

JavaScript · loops.js
const products = [
  { name: 'Notebook', price: 3 },
  { name: 'Pencil', price: 1 },
  { name: 'Stapler', price: 9 }
];

let total = 0;
for (const product of products) {
  console.log(product.name);
  total = total + product.price;
}
console.log(total);

let count = 3;
while (count > 0) {
  console.log(count);
  count = count - 1;
}
Shell · output
$ node loops.js
Notebook
Pencil
Stapler
13
3
2
1

Python

total = 0
for product in products:
    print(product['name'])  # → Notebook, Pencil, Stapler
    total = total + product['price']

print(total)  # → 13

JavaScript

let total = 0;
for (const product of products) {
  console.log(product.name); // → Notebook, Pencil, Stapler
  total = total + product.price;
}
console.log(total); // → 13
  • for (const product of products) runs its block once for each item in the array, in order. Each time, product is the name of the current item.
  • total is made with let before the loop, because every pass gives it a new value.
  • while (count > 0) runs its block again and again for as long as the condition is true. Something inside must change the condition, or the loop never ends.

You will meet for in the Week 4 tests, where it saves products one at a time, and while once in Week 3, where it reads database results one at a time. Most course code goes through an array another way, with functions. The next two topics build up to that.

In short

for...of runs a block once per array item. while repeats as long as its condition is true. A value that builds up across passes is made with let before the loop.

CHECK · predict before you choose

JavaScript
const quantities = [2, 1, 3];
let total = 0;
for (const quantity of quantities) {
  total = total + quantity;
}
console.log(total);
What does this file print when Node runs it?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • 6 — The loop adds 2, then 1, then 3. console.log comes after the closing brace, so it runs once.
  • 3 — The loop does not keep only the last item. Each pass adds to the total that is already there.
  • 2, then 3, then 6 — That would need console.log inside the braces. Here it comes after them, so it runs once, when the loop is done.

Functions: instructions with a name

A function is a group of statements with a name. Writing the function does not run it. Calling it does: the name, then parentheses.

JavaScript · functions.js
function orderTotal(price, quantity) {
  return price * quantity;
}

const orderTotalArrow = (price, quantity) => {
  return price * quantity;
};

const orderTotalShort = (price, quantity) => price * quantity;

console.log(orderTotal(12, 2));
console.log(orderTotalArrow(12, 2));
console.log(orderTotalShort(12, 2));

const logOrder = (pizza) => {
  console.log('Order: ' + pizza);
};

const result = logOrder('cheese');
console.log(result);

const titleOf = book => book.title;
console.log(titleOf({ title: 'Dune', year: 1965 }));
Shell · output
$ node functions.js
24
24
24
Order: cheese
undefined
Dune

Python

def orderTotal(price, quantity):
    return price * quantity


orderTotalShort = lambda price, quantity: price * quantity

print(orderTotal(12, 2))  # → 24
print(orderTotalShort(12, 2))  # → 24

JavaScript

function orderTotal(price, quantity) {
  return price * quantity;
}

const orderTotalShort = (price, quantity) => price * quantity;

console.log(orderTotal(12, 2)); // → 24
console.log(orderTotalShort(12, 2)); // → 24

Without return, a function gives back undefined, as a Python one gives back None, and that is what result holds. An arrow function is a lambda that can also hold a block.

  • price and quantity are parameters: names for the inputs, usable only inside the function.
  • orderTotal(12, 2) calls the function. The values 12 and 2 are its arguments, matched to the parameters in order.
  • return hands a value back to the caller and ends the function. The call orderTotal(12, 2) becomes the value 24.

The file writes the same function three ways. The first uses the word function. The second is an arrow function: the parameters, then =>, then the body, stored under a name with const. The third is the short arrow form. When the body is a single value, leave out the braces and the word return, and that value is returned.

The course writes arrow functions almost everywhere. With exactly one parameter, the parentheses around it may be left out. book => book.title is a complete function that takes a book and returns its title.

In short

A function is named, reusable code. Parameters name its inputs, and return hands back a value. The short arrow form returns its one value. With braces you must write return, or the function gives back undefined.

CHECK · predict before you choose

JavaScript
const double = (n) => {
  n * 2;
};
console.log(double(4));
What does this file print when Node runs it?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • 8 — With braces, a value is handed back only by return. Only the short form without braces returns its value automatically.
  • undefined — The body is in braces and has no return, so the function gives back undefined.
  • 4 — A function does not hand its argument back. With no return it gives back undefined.

Callbacks: hand a function to another function

A function is a value too. It can be stored under a name, and it can be given to another function as an argument. A function you hand over for other code to call is a callback. You write it. The other code decides when it runs and what it receives.

First with an array. filter is a function that every array has. A function that belongs to a value like this is called a method. filter calls your callback once for each item and keeps the items where the callback returns true.

JavaScript · callbacks.js
const books = [
  { title: 'Dune', year: 1965 },
  { title: 'The Hobbit', year: 1937 },
  { title: 'Emma', year: 1815 }
];

const isOld = (book) => book.year < 1960;

const oldBooks = books.filter(isOld);
console.log(oldBooks);

const newBooks = books.filter((book) => book.year >= 1960);
console.log(newBooks);

books.forEach((book) => {
  console.log(book.title);
});
Shell · output
$ node callbacks.js
[ { title: 'The Hobbit', year: 1937 }, { title: 'Emma', year: 1815 } ]
[ { title: 'Dune', year: 1965 } ]
Dune
The Hobbit
Emma

Python

isOld = lambda book: book['year'] < 1960

oldBooks = list(filter(isOld, books))

newBooks = list(filter(lambda book: book['year'] >= 1960, books))
print(newBooks)  # → [{'title': 'Dune', 'year': 1965}]

JavaScript

const isOld = (book) => book.year < 1960;

const oldBooks = books.filter(isOld);

const newBooks = books.filter((book) => book.year >= 1960);
console.log(newBooks); // → [ { title: 'Dune', year: 1965 } ]
  • books.filter(isOld) hands over the function itself. There are no parentheses after isOld, because you are not calling it. filter calls it three times, with one book each time.
  • The second filter writes the callback in place instead of naming it first. This is the form the course uses most.
  • forEach calls its callback once per item and keeps nothing. It does the same job as a for loop.

Now the same idea in Express. This is the server from setup with two console.log lines added. Save it as server.js in your week1hello folder and run it. Then load http://localhost:3000 in a browser twice.

JavaScript · server.js in week1hello
import express from 'express';

const app = express();

app.get('/', (req, res) => {
  console.log('Handler ran');
  res.send('Hello World!');
});

app.listen(3000);
console.log('Waiting for requests');
Shell · output after two requests
$ node server.js
Waiting for requests
Handler ran
Handler ran

app.get is called with two arguments: the path '/' and a callback. Read the call as: when a GET request for this path arrives, run this function. Express keeps the callback and calls it once for each matching request. It passes in the request as req and a response to fill in as res. Course pages call this callback a handler. Flask marks one with a decorator instead.

The output shows the timing. Waiting for requests prints first, although it is the last line of the file, because registering the callback does not run it. Handler ran prints only when a request arrives, once per request. Press Ctrl+C to stop the server.

In short

A callback is a function you hand to other code to call later. filter calls yours once per item. app.get(path, handler) keeps yours and calls it each time a matching request arrives, not when the file starts.

CHECK · predict before you choose

JavaScript · a file in week1hello
import express from 'express';
const app = express();

app.get('/menu', (req, res) => {
  console.log('B');
  res.send('menu');
});

app.listen(3000);
console.log('A');
You start this file with Node. No request has been sent yet. What has printed?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • B, then A — app.get only registers the callback. Nothing inside it runs until a request arrives.
  • A, then B — The callback does not run once at startup. It runs only when a GET request for /menu arrives.
  • Only A — Registering a route does not run its callback. B prints later, once for each request to /menu.

Three shortcuts the course uses everywhere

These three are shorter ways to write things you already know. They appear in almost every course file.

JavaScript · syntax.js
const order = { pizza: 'cheese', quantity: 2 };

const { pizza, quantity } = order;
console.log(`Order: ${quantity} ${pizza}`);
console.log(`Total: ${quantity * 12} dollars`);

const size = 'large';
const summary = { pizza, size };
console.log(summary);

const { crust } = order;
console.log(crust);
Shell · output
$ node syntax.js
Order: 2 cheese
Total: 24 dollars
{ pizza: 'cheese', size: 'large' }
undefined

Python

order = {'pizza': 'cheese', 'quantity': 2}

pizza, quantity = order['pizza'], order['quantity']
print(f'Order: {quantity} {pizza}')  # → Order: 2 cheese

size = 'large'
summary = {'pizza': pizza, 'size': size}
print(summary)  # → {'pizza': 'cheese', 'size': 'large'}

JavaScript

const order = { pizza: 'cheese', quantity: 2 };

const { pizza, quantity } = order;
console.log(`Order: ${quantity} ${pizza}`); // → Order: 2 cheese

const size = 'large';
const summary = { pizza, size };
console.log(summary); // → { pizza: 'cheese', size: 'large' }
  • Destructuring. const { pizza, quantity } = order; makes two names from two fields with the same names. It means the same as const pizza = order.pizza; followed by const quantity = order.quantity;. A field that is not there gives undefined, as crust shows.
  • Template literal. A string written between backticks (`) instead of quotes can hold ${...}. JavaScript works out what is inside the braces and puts the result into the text.
  • Property shorthand. { pizza, size } means the same as { pizza: pizza, size: size }. When a field and the variable that holds its value share a name, write the name once.

Week 1 uses the first two together. const { itemid } = req.query; takes one field out of the request, and a template literal puts it into the reply.

Section 2 has a table of the array methods the course uses, such as find and map, and of two more shortcuts, spread and rest.

In short

Destructuring pulls fields into names. A template literal uses backticks and ${...} to put values into text. { pizza, size } is short for { pizza: pizza, size: size }. All three are shorthand, not new behavior.

CHECK · predict before you choose

JavaScript
const book = { title: 'Dune', year: 1965 };
const { title, author } = book;
console.log(`${title} by ${author}`);
What does this file print when Node runs it?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • Dune by — A missing value is not left blank. The template literal turns undefined into the text undefined.
  • An error, because book has no author — Destructuring a missing field is not an error. The name is created with the value undefined.
  • Dune by undefined — book has no author field, so author is undefined, and the template literal prints it as text.

Modules: one file uses another

A real project is many files. A module is one file of code. export marks what a file offers to other files, and import brings those names into another file.

A folder that uses import needs one setting first. Do what setup did. In the terminal, inside js-primer, run npm init -y. Then set "type": "module" in the new package.json file and create the two files below.

JavaScript · menu.js
export const menu = ['cheese', 'pepperoni', 'veggie'];

export const priceOf = (size) => {
  if (size === 'large') {
    return 15;
  }
  return 12;
};
JavaScript · order.js
import { menu, priceOf } from './menu.js';

console.log(menu[0]);
console.log(priceOf('large'));
Shell · output
$ node order.js
cheese
15

Python

# order.py, beside menu.py
from menu import menu, priceOf

print(menu[0])  # → cheese
print(priceOf('large'))  # → 15

JavaScript

// order.js, beside menu.js
import { menu, priceOf } from './menu.js';

console.log(menu[0]); // → cheese
console.log(priceOf('large')); // → 15

Python imports by module name, and every top-level name can be imported. JavaScript imports by path, and only the names marked export are offered.

  • import { menu, priceOf } from './menu.js'; asks for two names. The names in the braces must match the exported names exactly.
  • The part in quotes is a string that says where the code is. ./ means “in the same folder as this file”, and the file name is written in full, with .js at the end.
  • A string with no ./ names a package that npm installed in node_modules instead. That is why from 'express' and from './menu.js' look different.

Importing a file runs it once, from top to bottom, and then hands over its exports. A file that only defines things, like menu.js, prints nothing when it is imported.

Leave out the ./ and Node looks for a package instead of your file. order-broken.js is a copy of order.js that says from 'menu.js' instead, and it fails like this.

Shell · the line that matters, from a longer error; folder changed to the example folder
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'menu.js' imported from /Users/you/si679/js-primer/order-broken.js

In short

A module is a file. export offers a name, and import brings it into another file. The quoted part is a path: ./ means this folder, and no ./ means an installed package. Importing a file runs it once.

CHECK · predict before you choose

JavaScript · two files in the same folder
// special.js
export const special = 'veggie';
console.log('special.js ran');

// today.js
import { special } from './special.js';
console.log(special);
You run node today.js. What prints?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • special.js ran, then veggie — Importing special.js runs it once from top to bottom, so its console.log runs first. Then today.js carries on.
  • Only veggie — import does not copy one name and skip the rest. The whole imported file runs once.
  • veggie, then special.js ran — An imported file runs before the statements of the file that imports it.

Waiting for slow work: Promise, async, await

Some work is slow: asking a database, calling another server. JavaScript does not stop and wait for it. A function that starts slow work returns at once, and what it returns is a Promise: an object that stands for a result that is not ready yet. Later the Promise either delivers the value or fails with an error.

await is how you get the value. Written in front of a Promise, it pauses your function until the Promise is done, then gives you the value.

To try it without a database, save this stand-in as db.js. Its findProduct answers after a tenth of a second, the way a database call takes a moment. Then save product.js and run it.

JavaScript · db.js, a stand-in for a database
// db.js stands in for a database. Copy it as it is.
const products = [
  { id: 1, name: 'Notebook', price: 3 },
  { id: 2, name: 'Stapler', price: 9 }
];

const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

export const findProduct = async (id) => {
  await wait(100);
  for (const product of products) {
    if (product.id === id) {
      return product;
    }
  }
  return null;
};
JavaScript · product.js
import { findProduct } from './db.js';

const product = await findProduct(2);
console.log(product.name);

const showName = async (id) => {
  const found = await findProduct(id);
  console.log(found.name);
};
await showName(1);

const forgot = findProduct(2);
console.log(forgot);
console.log(forgot.name);
Shell · output
$ node product.js
Stapler
Notebook
Promise { <pending> }
undefined

Python

import asyncio
from db import findProduct
async def main():
    product = await findProduct(2)
    print(product['name'])  # → Stapler
    forgot = findProduct(2)
    print(forgot)  # → <coroutine object findProduct>
asyncio.run(main())

JavaScript

import { findProduct } from './db.js';

const product = await findProduct(2);
console.log(product.name); // → Stapler

const forgot = findProduct(2);
console.log(forgot); // → Promise { <pending> }

Python’s async def and await work the same way, but only Python warns about a coroutine that was never awaited.

Where await pausesTwo timelines, each read from top to bottom. With await, the line is const product = await findProduct(2). findProduct(2) returns a Promise at once. Then the timeline is dashed: await pauses your function here, until the Promise is done. After the pause, product is the product object, and console.log(product.name) prints Stapler. Without await, the line is const forgot = findProduct(2). findProduct(2) returns a Promise at once. There is no pause: the next line runs right away. forgot is the Promise, not the product, and console.log(forgot.name) prints undefined.With awaitconst product = await findProduct(2);findProduct(2) returns a Promise at once.await pauses your function here,until the Promise is done.product is the product object.console.log(product.name) prints StaplerWithout awaitconst forgot = findProduct(2);findProduct(2) returns a Promise at once.No pause: the next line runs right away.forgot is the Promise, not the product.console.log(forgot.name) prints undefined
Notice the dashed stretch: with await your function waits there for the value, and without it the next line runs while the Promise is still not done.
  • await findProduct(2) waits for the answer. After that line, product is the product object, so product.name prints Stapler as expected.
  • await works at the top of a module, as here, and inside a function marked async. In any other function it is an error. Course handlers that use a database are written async (req, res) => { ... } for this reason.
  • Every async function returns a Promise, so the call showName(1) is awaited too.
  • The last three lines leave await out, so forgot is the Promise itself. Node shows it as Promise { <pending> }, which means not finished yet. A Promise has no name field, so forgot.name gives undefined without any error.

That last case is the bug to remember for Week 3. Nothing crashes and no error prints. The code carries on with a Promise where it expected a product. When a value from the database is unexpectedly undefined, or prints as Promise, look for an await that is missing.

Rebuild it · put the lines in order

Await a value, then use it

Rebuild product.js from this topic, without its last three lines. Put the lines in order so the file prints Stapler, then Notebook.

Show the solved code
import { findProduct } from './db.js';
const product = await findProduct(2);
console.log(product.name);
const showName = async (id) => {
  const found = await findProduct(id);
  console.log(found.name);
};
await showName(1);

await hands over the value, so every line that uses a value comes after the line that awaits it, at the top of the file and inside showName alike.

In short

A Promise stands for a result that is not ready yet. await pauses an async function until the value arrives. Forget await and you hold the Promise, not the value: it prints as a pending Promise and its fields are undefined.

CHECK · predict before you choose

JavaScript · uses db.js from this topic
import { findProduct } from './db.js';

const product = findProduct(1);
console.log(product.price);
What does this file print when Node runs it?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • 3 — Without await, product is a Promise, not the product. The price 3 never reaches this line.
  • An error, because the product is not ready — A missing await does not raise an error here. That is what makes the bug hard to see.
  • undefined — findProduct returns a Promise. A Promise has no price field, so reading it gives undefined.

When code fails: throw, try, catch

An error stops the program at the line where it happens. The lines after it do not run. Save this as receipt.js and run it. The order has no customer field.

JavaScript · receipt.js
const order = { pizza: 'cheese', quantity: 2 };

const customerName = (order) => {
  return order.customer.name;
};

console.log('Before');
console.log(customerName(order));
console.log('After');
Shell · real output, folder changed to the example folder; your Node version may differ
$ node receipt.js
Before
file:///Users/you/si679/js-primer/receipt.js:4
  return order.customer.name;
                        ^

TypeError: Cannot read properties of undefined (reading 'name')
    at customerName (file:///Users/you/si679/js-primer/receipt.js:4:25)
    at file:///Users/you/si679/js-primer/receipt.js:8:13
    at ModuleJob.run (node:internal/modules/esm/module_job:439:25)
    at async node:internal/modules/esm/loader:643:26
    at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)

Node.js v24.19.0

Python

order = {'pizza': 'cheese', 'quantity': 2}
def customerName(order):
    return order['customer']['name']

print('Before')  # → Before
print(customerName(order))  # → KeyError: 'customer'
print('After')

JavaScript

const order = { pizza: 'cheese', quantity: 2 };
const customerName = (order) => {
  return order.customer.name;
};
console.log('Before'); // → Before
console.log(customerName(order));
// → TypeError: Cannot read properties of undefined (reading 'name')
console.log('After');

Python stops one step earlier, at the missing key, and its traceback lists the newest call last. Node lists it first.

Before printed and After did not. The program stopped in between. Read what Node printed in this order.

  1. Find the line that starts with the kind of error and a colon: TypeError: Cannot read properties of undefined (reading 'name'). It is the first line of the error itself, and it says what went wrong. Here the code tried to read name from a value that turned out to be undefined, which has no fields.
  2. Look at the lines above it. They give the file and the line number, receipt.js:4, and show that line with a ^ under the spot.
  3. The lines that start with at are the stack trace: the chain of calls that led here, newest first. Find the first ones that name your own file. receipt.js:4:25 is inside customerName. The next one, receipt.js:8:13, is the line that called it. Lines that mention node:internal belong to Node. Skip them.

order.customer was never set, and a value that is undefined has no fields to read. This one message, with a different word in the parentheses, is the error you will see most in this course.

throw lets your own code raise an error, and try with catch lets it catch one.

JavaScript · quantity.js
const checkQuantity = (quantity) => {
  if (quantity < 1) {
    throw new Error('Quantity must be at least 1');
  }
  return quantity;
};

try {
  console.log(checkQuantity(2));
  console.log(checkQuantity(0));
  console.log('This line is skipped');
} catch (error) {
  console.log('Caught: ' + error.message);
}

console.log('Still running');
Shell · output
$ node quantity.js
2
Caught: Quantity must be at least 1
Still running
  • throw new Error('...') creates an error with your message and stops the function right there.
  • When anything inside the try block throws, the rest of that block is skipped and the catch block runs. error.message is the text of the error.

When a Promise fails, the await in front of it throws at that line, so the same try and catch handle a database call that goes wrong.

In short

An error stops the program where it happens. Read the line with the kind of error and its message first, then the first at line that names your file. throw raises an error; try and catch let the program carry on.

CHECK · predict before you choose

JavaScript
try {
  console.log('A');
  throw new Error('stop');
  console.log('B');
} catch (error) {
  console.log('C');
}
console.log('D');
What does this file print when Node runs it?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • A, then B, then C, then D — throw stops the try block at once. The line after it never runs.
  • A, then C, then D — throw skips the rest of try, catch runs, and the program carries on after it.
  • A, then C — A caught error does not end the program. The line after the catch block runs as normal.

Source trail · General JavaScript usage, not course text. Examples reuse this guide’s pizza order, books and products. Every example and every output shown was run with Node 24 on one Mac.

01

Week 1 · HTTP, Node, Express, forms, and JSON

A request becomes a response.

Start with the conversation

Connection to Section 0: the menu conversation now gets its real web names and code. If client, API, or request still feels fuzzy, revisit the pizza story first.

Where this chapter sitsA client sends a request to the server. It passes route, middleware, controller and service; the service uses the database, and the response goes back to the client. Highlighted in this chapter: the request and the response.Clientbrowser, Postman, curlrequestresponseServerRouteMiddlewareControllerServiceDatabasethis chapter
This chapter lights up the two arrows: the request a client sends and the response the server returns.

Label the map · tap a name, then a slot

Name the parts of one request

Put each name where it belongs on the map.

The chapter map with its names blankedThe same map as above, with 4 numbered slots where its names were.1?browser, Postman, curl2?3?Server4?MiddlewareControllerServiceDatabase

This exercise needs JavaScript. The map above shows every name in its place.

A client asks for something. A server runs code and sends an answer. A browser, Postman, curl, and a Supertest test can all act as clients. An API is the agreement about which requests are allowed and what responses mean.

Node runs JavaScript outside the browser. Express is a library that helps a Node program receive HTTP requests, match routes, parse data, and send responses.

One request, one responseA client and a server exchange two messages. First, the client sends one HTTP request. It has a method, POST; a path, /tracks; headers, Host and Content-Type; and a body, the track as JSON. On the server, the Express handler runs. Second, the server sends back one HTTP response. It has a status, 200 OK; headers, Content-Type; and a body, the added track as JSON.ClientServer1HTTP requestMethodPOSTPath/tracksHeadersHost: localhost:3000Content-Type: application/jsonBody{"artist":"Radiohead","title":"Creep","year":1992}Express handler2HTTP responseStatus200 OKHeadersContent-Type: application/jsonBodythe added track, as JSON
Notice that each message is made of labelled parts, and that one request gets exactly one response.
HTTP request
POST /tracks HTTP/1.1
Host: localhost:3000
Content-Type: application/json

{"artist":"Radiohead","title":"Creep","year":1992}

Line by line

  1. POST /tracks HTTP/1.1The start line. The method is POST, the path is /tracks, and HTTP/1.1 names the protocol.
  2. Host: localhost:3000A header. It names the host and port the request is for. Here localhost means your computer, and a port chooses the listening program.
  3. Content-Type: application/jsonAnother header. Headers describe the request; this one says the body is JSON.
  4. {"artist":"Radiohead","title":"Creep","year":1992}The body, after one blank line. It carries the submitted track.
JavaScript · composed for this guide, run with Express 5.2
import express from 'express';
const app = express();
app.use(express.json());

app.post('/tracks', (req, res) => {
  const { artist, title, year } = req.body;
  res.json({ artist, title, year });
});

app.listen(3000);

Python

@app.post('/tracks')
def tracks():
    body = request.get_json()
    artist, title, year = body['artist'], body['title'], body['year']
    return {'artist': artist, 'title': title, 'year': year}  # → 200 {"artist":"Radiohead","title":"Creep","year":1992}

JavaScript

app.use(express.json());
app.post('/tracks', (req, res) => {
  const { artist, title, year } = req.body;
  res.json({ artist, title, year });
});

Without the Content-Type header, Flask answers 415 and this route 500: req.body stays undefined.

A URL includes protocol, host, optional port, path, and optional query.

PartExampleMeaning
MethodGET, POSTRead data or submit data; method + path determine the route.
Path/tracksWhich endpoint the client is asking for.
Query?author=Tolkien&year=1937Named URL inputs, often filters. Separate pairs with &.
HeadersContent-Type: application/jsonMetadata; describes the submitted body format.
Body{"title":"Dune"}Submitted content; common with POST and PATCH.
Response status201 CreatedWhat happened, independent of the visible body.

In short

An HTTP request has a method, path, headers, and sometimes a body. The response reports a status and data. Think of an addressed envelope: destination and contents do different jobs.

CHECK · predict before you choose

You type /submitform into the browser address bar. What did you test?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • POST /submitform — Use a form, Postman, curl, or a test client to send POST.
  • Every route on that path — Route matching uses both method and path.
  • GET /submitform — The address bar sends GET, even if a POST route has the same path.

Know where the input lives

SourceRequest exampleRead it from
Path parameter/play/artist/Radiohead/song/Creepreq.params.artist and req.params.song
Query parameter/catalog?itemid=222req.query.itemid
Form bodyA form with name="zipcode"req.body.zipcode, after URL-encoded parsing
JSON bodyPOST /tracks with JSONreq.body.artist, after JSON parsing
HeaderAuthorization: Bearer SI679req.headers.authorization

A colon in /person/:id defines a placeholder. The client uses a real path such as /person/42; it does not literally send :id. URL encoding represents spaces as %20, which Express decodes. Route parameters are text; query values also need checking before you treat them as a single string or number.

Anatomy of a URLThe URL http://localhost:3000/play/artist/Radiohead followed by the query string title=Creep and year=1992, split into seven numbered parts. 1, the protocol, http. 2, the host, localhost, which is your computer. 3, the port, 3000, which chooses the listening program. 4, the path, /play/artist/Radiohead, which matches the route /play/artist/:artist. 5, the path parameter, Radiohead, which Express puts in req.params.artist. 6, the query parameter title=Creep, which Express puts in req.query.title. 7, the query parameter year=1992, which Express puts in req.query.year as text. A question mark starts the query string and an ampersand separates the pairs.One URL, shown on two lines5http://localhost:3000/play/artist/Radiohead1234?title=Creep&year=1992671Protocolhttp2Hostlocalhost is your computer3Port3000 chooses the listening program4Pathmatches the route /play/artist/:artist5Path parameterreq.params.artist is 'Radiohead'6Query parameterreq.query.title is 'Creep'7Query parameterreq.query.year is '1992', as text? starts the query string.& separates the pairs.
Notice that Express hands your code two parts of the URL as input: the path parameter in req.params and each query parameter in req.query, always as text.

In HTML, the form’s action chooses the URL, method chooses POST or GET, and each input’s name chooses the body key. An input’s id connects it to a label; it does not choose the submitted key. Keep ZIP codes as strings so "01234" keeps its first zero.

JavaScript · adapted teaching example
app.use(express.urlencoded({ extended: true })); // HTML form bodies
app.use(express.json());                        // JSON bodies

app.post('/submitform', (req, res) => {
  const { address, city, zipcode } = req.body;
  res.send(`Ship to ${address}, ${city}, ${zipcode}`);
});

Line by line

  1. app.use(express.urlencoded({ extended: true }));Runs before the routes on every request and parses an HTML form body into req.body.
  2. app.use(express.json());The same job for a body written as JSON.
  3. app.post('/submitform', (req, res) => {Registers a route: when a POST request for /submitform arrives, Express runs this function.
  4. const { address, city, zipcode } = req.body;Destructuring: three names from three body fields, each matching a form input’s name.
  5. res.send(`Ship to ${address}, ${city}, ${zipcode}`);Builds the reply with a template literal and sends it, which finishes the request.

Python

@app.post('/submitform')
def submitform():
    body = request.form or request.get_json()
    address, city, zipcode = body['address'], body['city'], body['zipcode']
    return f'Ship to {address}, {city}, {zipcode}'  # → 200 Ship to 1 Elm St, Troy, 48083

JavaScript

app.use(express.urlencoded({ extended: true })); // HTML form bodies
app.use(express.json());                        // JSON bodies

app.post('/submitform', (req, res) => {
  const { address, city, zipcode } = req.body;
  res.send(`Ship to ${address}, ${city}, ${zipcode}`);
});

Flask has no parser lines: it parses the body when the handler asks for request.form or request.get_json().

JSON is text with a defined syntax. It supports strings, numbers, booleans, null, arrays, and objects. Keys and string values use double quotes. Comments, trailing commas, and undefined are not valid JSON. JSON.parse() converts text to a value; JSON.stringify() converts a value to text.

In short

Put each input in the right drawer: path → req.params; query → req.query; parsed form/JSON → req.body; headers → req.headers. Form name chooses the submitted key. Validate values before using them.

CHECK · predict before you choose

GET /browse/books?keywords=coffee: where are the two inputs?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • category in query; keywords in params — That reverses the two locations; read the path and ? separately.
  • category in params; keywords in query — The path segment fills the placeholder; the text after ? is a query.
  • Both in the JSON body — These inputs are present in the URL, not a submitted JSON body.

Read a route from left to right

New to JavaScript? The JavaScript section teaches each piece of this code first, and its callbacks topic reads app.get the same way.

JavaScript · simplified from Week 1
import express from 'express';
const app = express();
app.use(express.json());

app.get('/catalog', (req, res) => {
  const { itemid } = req.query;
  if (typeof itemid !== 'string' || itemid.trim() === '' ||
      Number.isNaN(Number(itemid))) {
    res.status(400).send('Error: itemid must be a number');
    return;
  }
  res.send(`You asked for item ${itemid}`);
});

app.listen(3000);

Line by line

  1. import express from 'express';Loads the Express library so this file can use it.
  2. const app = express();Creates the app. Routes and middleware are attached to it.
  3. app.use(express.json());Parses JSON request bodies before any route runs.
  4. app.get('/catalog', (req, res) => {Here app.get registers a route. Its callback runs when a matching request arrives: the name req holds the request and res builds the response.
  5. const { itemid } = req.query;Destructuring gives a local name to an object property: here, the itemid query value.
  6. if (typeof itemid !== 'string' || itemid.trim() === '' ||Validation happens before using the input. This condition continues on the next line. It is true when itemid is not a single string, is blank, or does not convert to a number.
  7. res.status(400).send('Error: itemid must be a number');First res.status(400) sets the status, then send() sends the text. Calling status() alone does not finish a response.
  8. return;Stops the callback after the error response, preventing a second response.
  9. res.send(`You asked for item ${itemid}`);Reached only with valid input. A template literal uses backticks and ${...} to insert values.
  10. app.listen(3000);Starts the server. It now waits for requests on port 3000.

Rebuild it · put the lines in order

A route that validates before it answers

Rebuild the Week 1 catalog route. Put the lines in order so the handler reads the input, tests it, answers 400 and stops when it is bad, and answers with the item when it is good.

Show the solved code
app.get('/catalog', (req, res) => {
  const { itemid } = req.query;
  if (typeof itemid !== 'string' || itemid.trim() === '' ||
      Number.isNaN(Number(itemid))) {
    res.status(400).send('Error: itemid must be a number');
    return;
  }
  res.send(`You asked for item ${itemid}`);
});

Read, test, then answer: the 400 and its return come before the success line, so a bad itemid gets exactly one response and never reaches the code that uses it.

Python

from flask import Flask, request
app = Flask(__name__)
# no express.json() twin: request.get_json() parses on demand
@app.get('/catalog')
def catalog():
    itemid = request.args.get('itemid')
    return f'You asked for item {itemid}'  # → 200 You asked for item 222
app.run(port=3000)  # → * Running on http://127.0.0.1:3000

JavaScript

import express from 'express';
const app = express();
app.use(express.json());
app.get('/catalog', (req, res) => {
  const { itemid } = req.query;
  res.send(`You asked for item ${itemid}`);
});
app.listen(3000);

Three ways to answer: res.send() sends text or HTML, while res.json() serializes a value to JSON, and finally res.sendStatus(404) sets a status and sends its text label.

In short

A route pairs method + path with a handler. req reads the message; res builds the reply. Validate first, then return after sending an error so the handler cannot send twice. status() alone does not finish a response.

CHECK · predict before you choose

A handler calls res.status(400) and nothing else. Why might the client wait?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • req automatically sends the response — req holds input; res is the response interface.
  • The status was set, but no response was sent — Finish with send/json/end as appropriate; setting status alone is incomplete.
  • 400 always means the server must restart — 400 describes a bad request; it is not a restart instruction.

Every Week 1 activity, in one map

The week1hello code-along listens on 1679. The Week 1 Now You Try listens on 3000. Similar concepts do not make their paths interchangeable.

Code-along in week1helloWhat to understand
GET /, GET /aboutSimple text responses and route registration.
GET /contact → POST /submitRender a first/last-name form, then destructure fname and lname from the body.
GET /people?letter=aUse an array and find the first name with that initial, ignoring case. Missing/blank letter needs guarding; this starter demonstrates the idea rather than full validation.
GET /league/:leaguename/team/:teamnameTwo named path parameters in one route.
GET /person/:idConvert/check numeric input; reject unusable input with 400.
POST /postDataRead firstName and lastName from parsed JSON.
Week 1 Now You Try routeInput and result
GET /catalog?itemid=222Validate the query as nonblank numeric text; return the requested item sentence, or 400.
GET /play/artist/:artist/song/:songInsert both decoded path values into a listening sentence.
GET /getformReturn an address/city/ZIP form targeting POST /submitform.
POST /submitformUse form fields to describe the shipping destination.
GET /browse/:category?keywords=coffeeCombine one path parameter and one query input.
GET /hithere?name=AdaReturn a greeting using the query value.
POST /tracksRequire nonblank string artist and title; return status, trackAdded, and an ISO timestamp. Local success is 200.
GET /products/:department/:category?keywords=lcd,smart,sonyUse split(",") then join(" AND ") to produce lcd AND smart AND sony.
Predict: GET /catalog with no itemid. Why?

400. The value is not a string, so the guard stops the handler before conversion or the success response.

In short

Week 1 combines simple text routes, path/query inputs, forms, and JSON. Keep each project’s exact method, path, port, and response contract. A familiar route name is not enough to predict its behavior.

CHECK · predict before you choose

GET /catalog has no itemid. What should the saved Week 1 guard do?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • Convert the missing value to item zero — Missing input fails the guard; it is not an intentional item id.
  • Always return 404 — This case is invalid input, not a completed lookup for an absent resource.
  • Return 400 before conversion — The guard rejects a missing nonblank string before Number() and success.

Setup and manual client practice

Shell · new practice project, not needed for the existing repos
mkdir week1hello
cd week1hello
npm init -y
npm install express
npm pkg set type="module"
# Create server.js and a start script: node --watch server.js
npm start

Line by line

  1. mkdir week1helloMakes a new folder for the project.
  2. cd week1helloMoves the terminal into that folder, so the commands below act on it.
  3. npm init -yCreates package.json, which describes the project and scripts.
  4. npm install expressAdds Express as a dependency. Dependencies go under node_modules.
  5. npm pkg set type="module"Writes "type":"module" into package.json, which selects ES-module import syntax.
  6. # Create server.js and a start script: node --watch server.jsA note to you, not a command. The start script uses Node’s watch mode so saved code restarts the process; an in-memory array is lost during that restart.
  7. npm startRuns the start script.

In Postman, create an SI 679 collection and week folder, set method and full URL, and send GET /. For POST, choose Body → raw → JSON, enter the data, and check status/body. Add the route before expecting it to work. The lecture uses port 3000, while your saved week1hello uses 1679.

For /people, try letter=c, f, and 3, then no letter or the wrongly named ltr. These expose the difference between “no matching person” and “input cannot safely be indexed.” The notes’ /people/33 example conflicts with their /person/:id route; use /person/33 when testing the actual registered route. The Week 1 product route’s placeholder names are local department/category; the public path examples are electronics/tvs and household/kitchen either way.

In short

Installing packages prepares a project; running its start script launches it. Use the correct port and method in your client. Watch-mode restarts discard process-local arrays, like clearing a temporary notepad.

CHECK · predict before you choose

You installed Express, but no program is listening. What is missing?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • Import records into MongoDB first for every Week 1 route — These introductory routes do not all require a database.
  • Run the server program that calls listen() — A library installation does not create a running listener.
  • Change every request to POST — Changing a method cannot start the receiving program.

Source trail · Week 1 course page, both local Express activities, and the Week 1 study plan.

02

TypeScript basics · the book catalog exercise

Types describe your assumptions.

What TypeScript changes

TypeScript adds compile-time checking to JavaScript. It helps catch a misspelled field, the wrong argument type, or a possibly missing value before running the program. The compiler emits JavaScript, which Node executes. Type annotations disappear at runtime; they do not validate a request from a client.

Where this chapter sitsA client sends a request to the server. It passes route, middleware, controller and service; the service uses the database, and the response goes back to the client. This chapter adds a TypeScript note under the server: types check the code while you write it.Clientbrowser, Postman, curlrequestresponseServerRouteMiddlewareControllerServiceDatabaseTypeScript typeschecked while you write the codethis chapter
This chapter adds the TypeScript note: types check the server’s code while you write it and are gone when it runs.

Label the map · tap a name, then a slot

What runs, and what does not

Put each name where it belongs on the map.

The chapter map with its names blankedThe same map as above, with 5 numbered slots where its names were.1?browser, Postman, curl2?3?Server4?MiddlewareControllerService5?

This exercise needs JavaScript. The map above shows every name in its place.

Python

def add_order(quantity: int) -> dict:
    return {'quantity': quantity}

print(add_order('-2'))  # → {'quantity': '-2'}

TypeScript

interface Order { quantity: number }
function addOrder(quantity: number): Order {
  return { quantity };
}
addOrder('-2');
// → tsc: error TS2345: Argument of type 'string'
//   is not assignable to parameter of type 'number'.

Python type hints are optional and ignored at runtime, so the Python call runs. The drawing below shows the TypeScript side: annotations are checked by tsc and then erased, so at runtime both languages still need to validate incoming data.

The exercise starts with a working src/index.js book catalog and converts it to src/index.ts while preserving its output. The six numbered tasks are interface, typed array, enum, function signatures, union alias, and generic. You need to understand all six.

TypeScript
enum BookStatus {
  Available = "available",
  CheckedOut = "checked-out",
  Lost = "lost"
}

interface Book {
  title: string;
  author: string;
  year: number;
  status: BookStatus;
}

const catalog: Book[] = [];

function findBookByTitle(catalog: Book[], title: string): Book | undefined {
  return catalog.find(book => book.title === title);
}

Line by line

  1. enum BookStatus {An enum gives names to allowed status values. This line and the three below it list every status a book can have.
  2. interface Book {The words interface Book describe the fields of the object. Nothing is created; values are checked against it.
  3. year: number;One field: its name, a colon, then its type.
  4. status: BookStatus;A field can use a type you declared. Only the three enum values fit here.
  5. const catalog: Book[] = [];The type Book[] is an array of books, empty at the start. The word const prevents reassigning the variable, but you can still push into its array or change the fields of an object.
  6. function findBookByTitle(catalog: Book[], title: string): Book | undefined {The signature of the function. It describes two inputs and, after the last colon, a result that can be either a Book or undefined.
  7. return catalog.find(book => book.title === title);Returns the first book whose title matches. When none matches, find gives undefined, which is why the signature allows both.
Types are gone at runtimeA TypeScript file declares addOrder with quantity: number. The tsc compiler turns it into a JavaScript file where quantity has no type. Node runs that JavaScript. A request arrives with quantity set to the string "-2". Nothing in the running program checks it, so the code still needs runtime validation, which rejects it with 400.order.tsfunction addOrder(quantity: numbertype annotation) { … }tsccompilesorder.jsfunction addOrder(quantitytype gone) { … }runs in NodeRunning JavaScriptno types left to checkrequest{"quantity":"-2"}still needs validationRuntime validationa string, not a number: reply 400
Notice that the type annotation is gone before the request arrives, so only a runtime check can reject "-2".

In short

TypeScript checks your code before it runs; Node runs the emitted JavaScript. A type is a blueprint, not a security guard at the door. Incoming requests still need runtime validation.

CHECK · predict before you choose

A Product annotation says price is number. Postman sends "cheap". What protects the running API?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • Runtime checks on the received value — Annotations are erased; inspect type, finiteness, and domain rules at runtime.
  • The annotation alone — A compile-time declaration does not inspect network data.
  • const makes all object fields valid — const prevents rebinding, not malformed fields or object mutation.

Narrow a union before using it

TypeScript
type BookFilter = string | number;

function findBooksBy(catalog: Book[], filter: BookFilter): Book[] {
  if (typeof filter === "number") {
    return catalog.filter(book => book.year === filter);
  }
  return catalog.filter(book => book.author === filter);
}

const maybeBook = findBookByTitle(catalog, "Dune");
if (maybeBook !== undefined) {
  console.log(maybeBook.title);
}
console.log(maybeBook?.status);

Line by line

  1. type BookFilter = string | number;The type string | number is a union: one of those possibilities. The word type gives the union a name.
  2. function findBooksBy(catalog: Book[], filter: BookFilter): Book[] {The filter may be text or a number, so the code must find out which before using it.
  3. if (typeof filter === "number") {Checking with typeof narrows the union. Inside this branch TypeScript knows the filter is a number.
  4. return catalog.filter(book => book.year === filter);So it can be compared with year, which is a number.
  5. return catalog.filter(book => book.author === filter);Reached only when the check above was false. Here the filter must be a string, so it is compared with author.
  6. const maybeBook = findBookByTitle(catalog, "Dune");The result is a Book or undefined, as the signature of that function says.
  7. if (maybeBook !== undefined) {Narrowing again. Inside the braces the value is a Book, so reading its title is allowed.
  8. console.log(maybeBook?.status);Here ?. is optional chaining: if the value is null or undefined, stop accessing and yield undefined. It avoids a crash but does not guarantee that a value exists.

Python

BookFilter = str | int

def findBooksBy(catalog: list[Book], filter: BookFilter) -> list[Book]:
    if isinstance(filter, int):
        return [book for book in catalog if book.year == filter]
    return [book for book in catalog if book.author == filter]

print([book.title for book in findBooksBy(catalog, 1965)])  # → ['Dune']

TypeScript

type BookFilter = string | number;

function findBooksBy(catalog: Book[], filter: BookFilter): Book[] {
  if (typeof filter === "number") {
    return catalog.filter(book => book.year === filter);
  }
  return catalog.filter(book => book.author === filter);
}

A failed .find() returns undefined. A failed Mongo findOne() returns null. Both mean absence, but they are distinct values; declare the one the code returns.

In short

A union is a choice of possible types. Narrow it with a check before using type-specific operations. Optional chaining avoids a null/undefined access; it does not create missing data.

CHECK · predict before you choose

value is string | number. When is value.toUpperCase() safe?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • Inside a typeof value === "string" branch — The check narrows the value to the type that has this method.
  • Whenever the union mentions string — The other possibility is still a number until you narrow it.
  • After optional chaining, regardless of type — Optional chaining checks absence; it does not prove the value is a string.

Preserve a type with a generic

TypeScript
function getFirstItem<Item>(items: Item[]): Item | undefined {
  return items[0];
}

const firstNumber = getFirstItem([1, 2, 3]); // number | undefined
const firstBook = getFirstItem(catalog);   // Book | undefined

Line by line

  1. function getFirstItem<Item>(items: Item[]): Item | undefined {The name Item is a type parameter, chosen or inferred for each call. The element type of the input array becomes the output type.
  2. return items[0];The first element. An empty array has none, which explains the undefined possibility.
  3. const firstNumber = getFirstItem([1, 2, 3]);TypeScript infers Item as number from the argument, so the result is number or undefined.
  4. const firstBook = getFirstItem(catalog);The same function, with Item now inferred as Book.

Python

from typing import TypeVar
Item = TypeVar('Item')

def getFirstItem(items: list[Item]) -> Item | None:
    return items[0] if items else None  # [][0] raises IndexError

firstNumber = getFirstItem([1, 2, 3])  # int | None
print(firstNumber, getFirstItem([]))  # → 1 None

TypeScript

function getFirstItem<Item>(items: Item[]): Item | undefined {
  return items[0];
}

const firstNumber = getFirstItem([1, 2, 3]); // number | undefined

If the function used any instead, TypeScript would stop protecting the connection between input and output.

Type / syntaxRead it asUsed for
voidNo useful returned valueaddBook in the Week 2 service.
booleantrue or falsecheckoutBook and returnBook report success.
unknownA value we must inspect before useChecking untrusted topping entries in HW1.
anyDisable type checking for this valueA broad input can still be runtime-checked; prefer precise types when possible.
field?: stringThe field may be absentOptional order customizations or update fields.
Partial<Product>Every Product field becomes optionalWeek 3 updates and Week 4 supplied fields.
Omit<Book, "id">All Book fields except idDescribe editable book data.
Partial<Omit<Book, "id">>Optional changes without the identity fieldWeek 2 updateBook signature.
Promise<Product | null>An eventual product or absenceWeek 4 asynchronous get service.

In short

A generic keeps a type relationship: firstItem with Book returns Book or undefined. Unlike any, it preserves useful checks. Partial makes fields optional; Promise describes a result that arrives later.

CHECK · predict before you choose

A generic firstItem receives an empty Book array. What return type fits?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • Always string — A generic preserves the input/output type relationship, not a fixed string type.
  • Book | undefined — The element type stays Book, but an empty array has no first item.
  • Always Book — That promise ignores the empty-array case.

The JavaScript toolbox behind every week

This table builds on the JavaScript section, which teaches values, objects, arrays, functions and callbacks first.

OperationWhat it returns / doesExample meaning
find(predicate)First match or undefinedFind one menu item by id.
filter(predicate)New array of all matchesKeep books by an author; remove a book by keeping the other ids.
map(callback)New array with one transformed result per itemDocuments → Products, or Products → manufacturer names.
forEach(callback)Runs work for each item; returns undefinedCollect every validation problem.
some(predicate)Boolean: at least one matchDoes a size option exist?
every(predicate)Boolean: all elements passDo all filtered items have the requested category?
includes(value)Boolean membershipIs a crust offered?
Object.assign(book, changes)Copies fields into an existing objectWeek 2 partial updates mutate the found book.
{ ...fields, id }Copies enumerable fields into a new object; later keys winReplace a candidate id with the inserted id.
const { id, ...changes } = bodySeparate id from remaining propertiesWeek 2 PATCH body.
value ?? fallbackFallback only for null or undefinedPreserve price 0, unlike a truthiness-based default.
JavaScript · composed for this guide, run with Node 24
const books = [
  { title: 'The Hobbit', author: 'Tolkien', year: 1937 },
  { title: 'Dune', author: 'Herbert', year: 1965 },
];
const hobbit = books.find(book => book.year === 1937);
const herbert = books.filter(book => book.author === 'Herbert');
const asText = books.some(book => book.year === '1937');
console.log(hobbit.title, herbert.length, asText);

Python

books = [{'title': 'The Hobbit', 'author': 'Tolkien', 'year': 1937},
         {'title': 'Dune', 'author': 'Herbert', 'year': 1965}]
hobbit = next(book for book in books if book['year'] == 1937)
herbert = [book for book in books if book['author'] == 'Herbert']
asText = any(book['year'] == '1937' for book in books)
print(hobbit['title'], len(herbert), asText)  # → The Hobbit 1 False

JavaScript

const hobbit = books.find(book => book.year === 1937);
const herbert = books.filter(book => book.author === 'Herbert');
const asText = books.some(book => book.year === '1937');
console.log(hobbit.title, herbert.length, asText); // → The Hobbit 1 false

With no match, next() raises StopIteration where find returns undefined; pass a default, next(..., None).

async always returns a Promise; await pauses that async function until the Promise settles. It does not freeze the entire Node server.

With these projects’ ES-module/NodeNext setup, local TypeScript imports commonly end in .js, because Node loads the compiled JavaScript. import type brings in declarations erased at runtime; an ordinary import brings in usable runtime code. HW1 uses its supplied extensionless import configuration, so follow the repository’s existing setup.

In short

find chooses one; filter keeps matches; map transforms each item. Use ?? for a null/undefined fallback so zero survives. await waits within an async function; it does not stop the whole server.

CHECK · predict before you choose

A free product has price 0. Which expression preserves it?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • price || 10 — Zero is falsy, so || replaces it with 10.
  • price.map(10) — map transforms arrays; a numeric price is not an array.
  • price ?? 10 — Zero is present; ?? falls back only for null or undefined.

Explain the catalog’s behavior

addBook pushes a Book. findBookByTitle finds one or returns undefined. checkoutBook succeeds only for an existing available book; a second checkout returns false. returnBook restores an existing book to available. Filtering accepts either an author string or publication year number. The generic returns the first item safely.

The demo has The Hobbit, Dune, Foundation, and Children of Time. Foundation begins lost. Dune’s first checkout is true and its second is false; after returning it, its status is available. Converting to TypeScript should not change those runtime results or the README’s expected output.

Why can a TypeScript Product annotation not reject {"price":"cheap"} from Postman?

The annotation is erased from the running program. Incoming data must be inspected with runtime checks such as typeof, Number.isFinite, and Array.isArray.

In short

Converting the book catalog to TypeScript should preserve behavior. A first checkout succeeds only if the book is available; another checkout fails until it is returned. Types explain this state; they do not replace it.

CHECK · predict before you choose

Dune is checked out successfully, then checked out again without a return. Predict the second result.

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • TypeScript automatically returns the book — The compiler does not perform the application’s return operation.
  • false — The existing book is no longer available, so the rule rejects another checkout.
  • true because the book still exists — Existence and availability are separate conditions.

Source trail · TypeScript README, src/index.ts, its study plan, and type usage in Weeks 2–4/HW1.

2+

After TypeScript basics · twelve shapes, the delete-the-types trick, and reading red errors

Read any line.

The one trick: delete the types

TypeScript is JavaScript plus notes for the checker. When a line looks like a wall of symbols, delete the notes in your head. What is left is the JavaScript that Node runs, and most lines turn out to be short. Types describe your assumptions shows why: the notes are erased before the program runs (the erased-type drawing).

Delete these:

  • : string or any other type after a colon that follows a name, a parameter or the () of a function.
  • <Product> and anything else in angle brackets after a name, as in Promise<Product>. Delete the brackets too.
  • Whole lines that start with import type or type, like type Role = 'customer' | 'staff';. They never run.

Two exceptions trip everyone up:

  1. Object-literal colons stay. In { id: user.id } the colon pairs a field with its value. That is JavaScript, not a type.
  2. as const goes. It looks like code, but it only tells the checker that a value never changes. import * as name is different: it is a real JavaScript rename, so it stays.

Here is a Week 6 function that makes middleware, before and after:

TypeScript · Week 6 · src/middleware/require-role.ts:10–11
export const requireRole = (role: Role): Middleware => {
  return (req: Request, res: Response, next: NextFunction): void => {
JavaScript · types deleted
export const requireRole = (role) => {
  return (req, res, next) => {

And a line where every colon stays, because they are all inside an object literal:

TypeScript · Week 6 · src/services/auth-service.ts:41
  return { id: user.id, username: user.username, role: user.role };

Types deleted: the same line. It is plain JavaScript.

In short

Read TypeScript by deleting its notes: types after a colon, angle brackets, import type and type lines, and as const. What is left is the JavaScript that runs. Colons inside an object literal pair a field with a value, so they stay.

CHECK · predict before you choose

Delete the types from Week 6’s return { id: user.id, username: user.username, role: user.role };. What runs?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • return { id, username, role }; — That drops the values. The colons here pair fields with values; they are not types.
  • The same line, unchanged — Every colon is inside an object literal, so nothing on this line is a type note.
  • return { user.id, user.username, user.role }; — That is not valid JavaScript. An object literal needs a field name before each value.

The twelve shapes

The class code uses the same dozen shapes over and over. They are ordered here by how often each appears in the course’s .ts files; the counts are approximate. Each one shows a real line with its file, how to say it out loud, and the line with its types deleted.

1. Arrow function · about 380 times

TypeScript · Week 6 · src/config.ts:6
export const getJwtSecret = (): string => {

Say it:“Make a function called getJwtSecret. It takes nothing and gives back a string.”

  • const name = (…) => means “this name holds a function”.
  • : string right after the () is the return type: what comes out.
JavaScript · types deleted
export const getJwtSecret = () => {

2. Destructuring · about 168 times

TypeScript · Week 2 · src/books-router.ts:53
  const { title, author, year } = req.query;

Say it:“Take the fields named inside the braces out of req.query, and put each one in a variable of the same name.” It is short for writing const title = req.query.title; once per field.

Types deleted: the same line. It is plain JavaScript.

3. Template literal · about 93 times

TypeScript · Week 6 · src/middleware/require-role.ts:13
      throw new AuthorizationError(`Only ${role} may do this`);

Say it:“Build a string. Where it says ${role}, put in the value of role.” If role is staff, the message reads Only staff may do this. Backticks, not quotes, turn this on.

Types deleted: the same line. It is plain JavaScript.

4. Generics and unions · about 63 times

TypeScript · Week 6 · src/db/products-repository.ts:29
export const findProduct = async (id: string): Promise<Product | null> => {

Say it:findProduct takes an id that is a string. It gives back a promise that will later hold a Product, or null when nothing matches.

  • <…> reads “of”. Promise<Product> is a promise of a product, and Partial<Product> is a product whose fields are all optional.
  • | reads “or”.
JavaScript · types deleted
export const findProduct = async (id) => {

5. import type · about 47 times

TypeScript · Week 6 · src/middleware/require-jwt.ts:1
import type { NextFunction, Request, Response } from 'express';

Say it:“From Express, bring in three type names. They are used in annotations like req: Request, never as values.” The class tsconfig turns on verbatimModuleSyntax, so an import of types only must say so with the word type.

Types deleted: the whole line goes. It never runs.

6. The ?? operator · about 44 times

TypeScript · Week 6 · src/services/products-service.ts:52
  const quantity = changes.quantity ?? existing.quantity;

Say it:“Use the quantity that was sent. If it is missing, meaning undefined or null, keep the existing quantity.” It differs from || on zero, and zero is a real quantity: 0 ?? 5 gives 0, while 0 || 5 gives 5.

Types deleted: the same line. It is plain JavaScript.

7. as, which does two jobs · about 35 times

TypeScript · Week 3 · src/__tests__/products-changes.test.ts:73
  it.each(['get', 'patch', 'delete'] as const)('%s answers 400', async (method) => {

Say it:as const tells TypeScript this list is exactly these three words and nothing else. That is what lets request(app)[method] on the next line type-check. Most as lines in the class code do a different job, a renamed import:

TypeScript · Week 6 · src/routes/products-router.ts:2
import * as productsController from '../controllers/products-controller.js';

Say it:“Bring in everything that file exports, as one object named productsController.” Then productsController.getProducts is one of those exports.

JavaScript · types deleted
  it.each(['get', 'patch', 'delete'])('%s answers 400', async (method) => {

The as const goes. The import * as line stays exactly as it is.

8. .map, .filter and .find · about 34 times

TypeScript · Week 6 · src/services/products-service.ts:34
  return products.filter((product) => product.quantity < threshold);

Say it:“Go through products one at a time, keep only the ones whose quantity is below the threshold, and return that shorter list.”

  • .filter(test) keeps the items that pass the test.
  • .map(change) turns every item into something new. Week 6’s repository does docs.map((doc) => toProduct(doc)), so every MongoDB document becomes a Product.
  • .find(test) returns the first match, or undefined when nothing matches, as in Week 2’s book service.

Types deleted: the same line. It is plain JavaScript.

9. Optional chaining · about 9 times

TypeScript · Week 6 · src/controllers/auth-controller.ts:10
    req.body?.username,

Say it:“If there is a body, read its username. If there is no body at all, give undefined instead of crashing.” Without ?., reading req.body.username throws when req.body is undefined.

Types deleted: the same line. It is plain JavaScript.

10. Spread and rest · about 9 times

TypeScript · Week 4 · src/services/product-service.ts:19–21
  const { id, ...fields } = productFromFields(productInfo);
  const { insertedId } = await db.addToCollection(db.PRODUCTS, fields);
  return productFromFields({ ...fields, id: insertedId.toString() });

Say it:the first line is rest: “Pull id out, and collect every other field into fields.” The last line is spread: “Make a new object, copy every field of fields into it, then add an id.” Same three dots, opposite directions: on the left of the equals sign they collect, and inside braces on the right they copy out.

Types deleted: the same lines. They are plain JavaScript.

11. class … extends Error · 5 times, and it matters

TypeScript · Week 6 · src/errors.ts:7
export class AuthenticationError extends Error {}

Say it:“Make a new kind of error called AuthenticationError. It is an ordinary Error with its own name.” The empty braces are fine: the name is the point, so the error handler can ask err instanceof AuthenticationError and answer with a 401 status.

Types deleted: the same line. It is plain JavaScript.

12. async, await, try and catch

TypeScript · Week 6 · src/middleware/require-jwt.ts:17–21
  try {
    res.locals.user = jwt.verify(token, getJwtSecret());
  } catch {
    throw new AuthenticationError('Invalid or expired token');
  }

Say it:“Try to verify the token. If verifying throws any error, catch it and throw our own 401 error instead.”

TypeScript · Week 6 · src/services/auth-service.ts:32
  const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);

Say it:“Start hashing, which is slow, wait for it to finish, then put the result in hashedPassword.” await works only inside a function marked async, which is error TS1308 below. Waiting for slow work explains the promise underneath.

Types deleted: the same lines. They are plain JavaScript.

In short

Twelve shapes cover most lines: arrow functions, destructuring, template literals, generics, import type, ??, as, list methods, optional chaining, spread and rest, error classes, and async with try and catch. Say each one out loud, then delete its types.

CHECK · predict before you choose

Week 6 runs const quantity = changes.quantity ?? existing.quantity; with changes.quantity set to 0 and existing.quantity set to 12. What is quantity?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • 12, because 0 counts as missing — That is how || behaves. ?? treats only undefined and null as missing.
  • undefined — Neither side is undefined, and ?? always returns one of its two sides.
  • 0, because ?? replaces only undefined or null — Zero is a real value, so the left side is kept.

Five more shapes you will meet

A. Controller anatomy

TypeScript · Week 6 · src/controllers/products-controller.ts:4–7
export const getProducts = async (
  req: Request,
  res: Response
): Promise<void> => {

Read it piece by piece:

PieceMeaning
exportother files may import it
const getProducts =the name
asyncit may use await inside, and it returns a promise
(req: Request, res: Response)two inputs: the request coming in and the response going out
: Promise<void>a promise of nothing: its job is to send a response, not to return a value
=> {the body starts here
JavaScript · types deleted
export const getProducts = async (req, res) => {

B. Object literals and res.status().json()

TypeScript · Week 6 · src/services/auth-service.ts:41
  return { id: user.id, username: user.username, role: user.role };
TypeScript · Week 6 · src/controllers/products-controller.ts:21
    res.status(404).json({ error: 'No product with that id' });

Say it:the first line builds a new object with three fields, which is how hashedPassword is left out on purpose. The second: “Set the status to 404, then send this object as JSON.” res.status(404) returns res itself, which is why .json can follow it on the same line.

Types deleted: the same lines. They are plain JavaScript.

C. import and export with .js

TypeScript · Week 6 · src/app.ts:2
import { productsRouter } from './routes/products-router.js';

Say it:“From my file routes/products-router, bring in what it exports as productsRouter.” The name in braces must match an export const productsRouter in that file. Why .js when the file ends in .ts? Weeks 2 to 6 use Node’s ES modules with "module": "nodenext". Node runs the compiled .js files, so the import names the file that exists after compiling. Leaving it off is error TS2835.

Types deleted: the same line. It is plain JavaScript.

D. describe, it and expect

TypeScript · Week 6 · src/__tests__/auth.e2e.test.ts:40–51
describe('GET /auth/me', () => {
  // #region valid-token
  it('200s with the username for a token we sign ourselves', async () => {
    const token = jwt.sign(payload, getJwtSecret());

    const res = await request(app)
      .get('/auth/me')
      .set('Authorization', `Bearer ${token}`);

    expect(res.status).toBe(200);
    expect(res.body.username).toBe('ada');
  });
  • describe('GET /auth/me', …) is a group of tests about one thing.
  • it('200s with …', async () => {…}) is one test. Its text is the promise it checks.
  • expect(x).toBe(y) means “x must be exactly y, or this test fails”. .toEqual compares the contents of objects and lists instead; Choose the matcher by what you mean has the full list.

E. Supertest: request(app)

TypeScript · Week 6 · src/__tests__/auth.e2e.test.ts:45–47
    const res = await request(app)
      .get('/auth/me')
      .set('Authorization', `Bearer ${token}`);

Say it:“Send a pretend GET request to /auth/me on my app, with this Authorization header, and wait for the answer.” No real server or port is needed. res.status and res.body are the answer, which the next lines check with expect.

In short

A controller is an exported async function of req and res that returns a promise of nothing. res.status() returns res, so .json() can follow. Local imports end in .js. Tests group with describe, state one promise per it, and check with expect after a Supertest request.

CHECK · predict before you choose

In Week 6’s res.status(404).json({ error: 'No product with that id' });, why can .json come straight after .status(404)?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • status returns res itself, so the next call goes to the same response — Each call in a chain is made on what the call before it returned, and status returns the response.
  • Express waits until the function ends and then runs every call — Express runs each call when it is reached. Chaining is plain JavaScript.
  • json is a method of the number 404 — Numbers have no json method. The call is made on what status returned, the response.

Read a red TypeScript error

Every error below is real. The mistake was typed into a copy of a class repo, npx tsc --noEmit was run, and its output is pasted unchanged. Then the copy was put back. Every tsc error has the same parts:

Terminal · tsc output
src/app.ts(3,28): error TS2835: Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './routes/auth-router.js'?
  • src/app.ts is the file, and (3,28) is line 3, column 28.
  • TS2835 is the code: a name you can search for.
  • The rest is the message.
  1. Go to the file and line first. Do not read the whole message yet.
  2. Read the message. It usually says X is not Y. Find X on that line.
  3. Fix the first error first, then run tsc again. One mistake often causes several errors below it.

Four words come up again and again. A type is what kind of value something is. Assignable means “fits into”. A parameter is the name a function gives its input, like id in (id: string) =>. An argument is the actual value passed in when the function is called.

1. TS2345 · Argument of type … is not assignable to parameter

TypeScript · Week 6 · src/controllers/users-controller.ts:6 · the mistake
  const user = await usersService.getUser(caller, req.params.id);
Terminal · tsc output
src/controllers/users-controller.ts(6,51): error TS2345: Argument of type 'string | string[]' is not assignable to parameter of type 'string'.
  Type 'string[]' is not assignable to type 'string'.

What it means: Express types a URL parameter as possibly a list of strings, and getUser accepts exactly one string.

Fix: turn it into one string first, as the class code does everywhere:

TypeScript · Week 6 · src/controllers/users-controller.ts:6
  const user = await usersService.getUser(caller, String(req.params.id));

2. TS2322 · Type … is not assignable to type …

TypeScript · Week 6 · src/services/auth-service.ts:35–39 · the mistake
  const user = await usersRepository.insertUser({
    username,
    hashedPassword,
    role: 'admin'
  });
Terminal · tsc output
src/services/auth-service.ts(38,5): error TS2322: Type '"admin"' is not assignable to type 'Role'.

What it means: Role allows only two words, so admin does not fit into the box marked Role:

TypeScript · Week 6 · src/models/user.ts:1
export type Role = 'customer' | 'staff';

Fix: use one of the allowed words, as the real code does with role: 'customer', or add admin to Role if you really mean a new role.

3. TS2339 · Property … does not exist on type …

TypeScript · Week 6 · src/services/auth-service.ts:55 · the mistake
  const matches = await bcrypt.compare(password, user.password);
Terminal · tsc output
src/services/auth-service.ts(55,55): error TS2339: Property 'password' does not exist on type 'User'.

What it means: a User has no field called password. It has hashedPassword:

TypeScript · Week 6 · src/models/user.ts:4–9
export type User = {
  id: string;
  username: string;
  hashedPassword: string;
  role: Role;
};

Fix: use the real field name, user.hashedPassword. This is TypeScript catching a typo for you.

4. TS18048 · … is possibly 'undefined'

TypeScript · Week 2 · src/books-router.ts:20 · the mistake
  if (authorization.split(' ')[1] === secretKey) {
Terminal · tsc output
src/books-router.ts(20,7): error TS18048: 'authorization' is possibly 'undefined'.

What it means: a request may arrive with no Authorization header, so req.headers.authorization is a string or undefined, and you cannot call .split on nothing.

Fix: check first, as Week 2’s real code does:

TypeScript · Week 2 · src/books-router.ts:20
  if (authorization && authorization.split(' ')[1] === secretKey) {

You may also see TS2532, “Object is possibly undefined”. Same idea, same fix.

5. TS2304 · Cannot find name …

TypeScript · Week 6 · src/middleware/error-handler.ts:2–6 · the mistake
import {
  AuthenticationError,
  AuthorizationError,
  ValidationError
} from '../errors.js';
Terminal · tsc output
src/middleware/error-handler.ts(26,22): error TS2304: Cannot find name 'ConflictError'.

What it means: line 26 still says err instanceof ConflictError, but the file never created that name and no longer imports it.

Fix: put ConflictError back in the import list.

6. TS7006 · Parameter … implicitly has an 'any' type

TypeScript · Week 6 · src/db/users-repository.ts:8 · the mistake
const toUser = (doc): User => {
Terminal · tsc output
src/db/users-repository.ts(8,17): error TS7006: Parameter 'doc' implicitly has an 'any' type.

What it means: TypeScript cannot tell what doc is, and the class tsconfig sets "strict": true, which forbids a silent guess.

Fix: say the type:

TypeScript · Week 6 · src/db/users-repository.ts:8
const toUser = (doc: Document): User => {

A callback such as docs.map((doc) => toProduct(doc)) needs no type on doc, because TypeScript already knows what is inside docs.

7. TS2835 and TS2307 · import path problems

TypeScript · Week 6 · src/app.ts:3 · the mistake
import { authRouter } from './routes/auth-router';
Terminal · tsc output
src/app.ts(3,28): error TS2835: Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './routes/auth-router.js'?
TypeScript · Week 6 · src/app.ts:3 · the mistake
import { authRouter } from './routes/auth-routes.js';
Terminal · tsc output
src/app.ts(3,28): error TS2307: Cannot find module './routes/auth-routes.js' or its corresponding type declarations.

What it means: the first leaves off the .js, which every local import needs when a repo uses "type": "module" with nodenext. The second names a file that does not exist: the real one is auth-router.ts.

Fix: end with .js and check the spelling against the files in src/routes:

TypeScript · Week 6 · src/app.ts:3
import { authRouter } from './routes/auth-router.js';

8. TS2741 · Property … is missing in type …

TypeScript · Week 6 · src/db/users-repository.ts:37–41 · the mistake
  return {
    id: result.insertedId.toString(),
    username: user.username,
    hashedPassword: user.hashedPassword
  };
Terminal · tsc output
src/db/users-repository.ts(37,3): error TS2741: Property 'role' is missing in type '{ id: string; username: string; hashedPassword: string; }' but required in type 'User'.

What it means: the function promises to return a User, and every User must have a role.

Fix: add the missing field:

TypeScript · Week 6 · src/db/users-repository.ts:37–42
  return {
    id: result.insertedId.toString(),
    username: user.username,
    hashedPassword: user.hashedPassword,
    role: user.role
  };

9. TS1308 · 'await' outside an async function

TypeScript · Week 6 · src/controllers/auth-controller.ts:18–22 · the mistake
export const postLogin = (
  req: Request,
  res: Response
): void => {
  const token = await authService.logIn(
Terminal · tsc output
src/controllers/auth-controller.ts(22,17): error TS1308: 'await' expressions are only allowed within async functions and at the top levels of modules.

What it means: await works only inside a function marked async, and async was removed.

Fix: put async back, which makes the function return a promise:

TypeScript · Week 6 · src/controllers/auth-controller.ts:18–21
export const postLogin = async (
  req: Request,
  res: Response
): Promise<void> => {

In short

A tsc error reads file(line,col), then a code, then a message. Go to the line first, find the X in X is not Y, and fix the first error before the others, because one mistake often causes several errors.

CHECK · predict before you choose

tsc prints src/services/auth-service.ts(55,55): error TS2339: Property 'password' does not exist on type 'User'. Where do you look first?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • The TS2339 page in the TypeScript documentation — The code is worth searching later, but the line tells you where the mistake is.
  • Line 55 of src/services/auth-service.ts, for the word password — The file and line come first, and X in the message is password.
  • Every file that uses the User type — Only line 55 is reported. The User type itself is fine.

Read a Vitest failure

The mistake: in a copy of the Week 2 repo, the GET /books/:id route was changed to answer 400 instead of 404 for a missing book. Then npx vitest run src/__tests__/books.test.ts was run.

TypeScript · Week 2 · src/books-router.ts:73–80 · the mistake
booksRouter.get('/:id', (req: Request, res: Response) => {
  const book = getBook(Number(req.params.id));
  if (!book) {
    res.sendStatus(400);
    return;
  }
  res.json(book);
});

This is the test that caught it:

TypeScript · Week 2 · src/__tests__/books.test.ts:62–72
describe('GET /books/:id', () => {
  it('returns the matching book and 404 for a missing id', async () => {
    const posted = await addBook();
    const found = await request(app).get(`/books/${posted.body.id}`);
    const missing = await request(app).get('/books/1');

    expect(found.status).toBe(200);
    expect(found.body.title).toBe('The Hobbit');
    expect(missing.status).toBe(404);
  });
});

The real output, trimmed: the run header and the router’s log lines are cut.

Terminal · Vitest output
 ❯ src/__tests__/books.test.ts (9 tests | 1 failed) 64ms
   ❯ GET /books/:id (1)
     × returns the matching book and 404 for a missing id 7ms

⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯

 FAIL  src/__tests__/books.test.ts > GET /books/:id > returns the matching book and 404 for a missing id
AssertionError: expected 400 to be 404 // Object.is equality

- Expected
+ Received

- 404
+ 400

 ❯ src/__tests__/books.test.ts:70:28
     68|     expect(found.status).toBe(200);
     69|     expect(found.body.title).toBe('The Hobbit');
     70|     expect(missing.status).toBe(404);
       |                            ^
     71|   });
     72| });

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯


 Test Files  1 failed (1)
      Tests  1 failed | 8 passed (9)
  • (9 tests | 1 failed): eight tests still pass. Only one cares about this route.
  • FAIL … > GET /books/:id > returns the matching book and 404 for a missing id: the path is file, then the describe name, then the it name. It tells you which promise broke.
  • expected 400 to be 404: the server sent 400 and the test wanted 404. Read it as “expected what we got to be what we wanted”.
  • - Expected and + Received: the minus line is what the test wanted, and the plus line is what really happened.
  • books.test.ts:70:28 and the caret: the exact expect that failed. Lines 68 and 69 passed, so the found book was fine.

Say it out loud: “The test asked for a book id that does not exist. My route answered 400, but the request was fine and the book is missing, which is a 404. I would change res.sendStatus(400) back to res.sendStatus(404) in the GET /:id route.”

In short

A Vitest failure names the file, the describe and the it, then says expected what we got to be what we wanted. Minus lines are what the test wanted, plus lines are what happened, and the caret points at the expect that failed.

CHECK · predict before you choose

Vitest prints AssertionError: expected 201 to be 200. What did the server actually send?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • 200 — 200 is what the test wanted. It comes second in the sentence.
  • Nothing; the test crashed before the request — A crash would print the error that was thrown, not an expected and received pair.
  • 201 — Expected comes first and holds what we got, so the server sent 201.

The blank-file drill

Reading a line is half of it. To check that you can also write it, rebuild one file from memory in a copy of a class repo:

  1. Copy the whole repo, so your real one is never touched.
  2. Save the file you are about to blank somewhere outside the repo.
  3. Blank it, but keep its import lines. They tell you what the file uses.
  4. Rewrite the rest from memory, without looking.
  5. Run tsc, then the tests. Fix the first error first.
  6. Diff your version against the original and read every difference.
  7. Copy the original back, or delete the copy.
Terminal · drill commands
cp -R si-679-f-26-week06-nyt-yourname ~/drill-week06
cd ~/drill-week06
cp src/services/products-service.ts ../products-service.original.ts
# open src/services/products-service.ts, keep the import lines, delete the rest, rewrite it
npx tsc --noEmit
npm test
diff ../products-service.original.ts src/services/products-service.ts
cp ../products-service.original.ts src/services/products-service.ts

In a copy of the Week 6 repo the whole suite, 45 tests, ran in about two seconds, so checking is cheap. One round takes 30 minutes:

  • Minutes 0 to 2: copy the repo and blank the file.
  • Minutes 2 to 22: rewrite it.
  • Minutes 22 to 25: run tsc and the tests until both pass.
  • Minutes 25 to 30: diff, write down the two lines you missed, and restore.

Blank the same file again next round until the diff is quiet, then move on. Start with short files such as src/errors.ts and src/config.ts, then a service, a controller, a router and a test. A line that differs from the original while tsc and the tests pass is a second correct answer, not a mistake.

In short

Copy a class repo, blank one file but keep its imports, rewrite it from memory, run tsc and the tests, diff against the original, and restore it. Thirty minutes a round, on the same file until the diff is quiet.

CHECK · predict before you choose

You blank src/services/products-service.ts in your copy. What do you keep?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • Only the import lines — The imports show what the file uses, and everything after them is what you practise writing.
  • Nothing, not even the imports — Without the imports you would be recalling package paths, not the code this drill practises.
  • Every function name, with empty bodies — Then the shape of the file is given away, and the drill checks much less.

Source trail · Class exercise code from Weeks 2, 3, 4 and 6, quoted from pinned snapshots of those repos. Every error and the test failure were made by typing the mistake into a copy of a class repo and running tsc 7.0 or Vitest 5.0 with Node 24 on one Mac.

03

Week 2 · routers, middleware, services, and first tests

Order is part of the behavior.

Organize routes without changing the API

Connection to Section 0: this chapter opens the kitchen door. Follow how a ticket is directed, checked, and handed to the code that does the work.

Where this chapter sitsA client sends a request to the server. It passes route, middleware, controller and service; the service uses the database, and the response goes back to the client. Highlighted in this chapter: middleware.Clientbrowser, Postman, curlrequestresponseServerRouteMiddlewareControllerServiceDatabasethis chapter
This chapter lights up the middleware stage: what runs between the route match and the handler.

Label the map · tap a name, then a slot

Where middleware sits

Put each name where it belongs on the map.

The chapter map with its names blankedThe same map as above, with 5 numbered slots where its names were.Clientbrowser, Postman, curl1?responseServer2?3?4?5?Database

This exercise needs JavaScript. The map above shows every name in its place.

TypeScript
// app.ts
app.use('/books', booksRouter);

// books-router.ts
const booksRouter = express.Router();
booksRouter.get('/:id', handler);
// A client sends GET /books/123, not GET /:id.

Line by line

  1. app.use('/books', booksRouter);In app.ts, the app supplies a mount prefix. Every request whose path starts with /books goes to the router.
  2. const booksRouter = express.Router();In books-router.ts. A Router collects related handlers.
  3. booksRouter.get('/:id', handler);The router supplies a relative path. Mounting at /books and defining /:id produces /books/:id. Defining /books again inside that router would double the prefix.
  4. // A client sends GET /books/123, not GET /:id.A client always sends the full path with a real id.

Python

# app.py
app.register_blueprint(booksRouter, url_prefix='/books')

# books_router.py
booksRouter = Blueprint('books', __name__)
@booksRouter.get('/<id>')
def handler(id): return f'Book {id}'  # → 200 Book 123

TypeScript

// app.ts
app.use('/books', booksRouter);

// books-router.ts
const booksRouter = express.Router();
booksRouter.get('/:id', handler);

Week 2 separates types.ts (Book shape), books-service.ts (private in-memory catalog and operations), books-router.ts (HTTP handlers), app.ts (assembly), and index.ts (listen on 6790). The service exports addBook, getAllBooks, getBook, updateBook, removeBook, and _resetCatalog. HTTP code asks the service for data rather than owning the array.

In short

A mounted router adds its prefix to each relative route. /books + /:id becomes /books/:id. Think of a building address plus a room number; repeating the building address produces the wrong destination.

CHECK · predict before you choose

A router mounted at /books defines GET /books/:id. Which URL results?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • /books/:id — That requires the router to define /:id, without a second /books.
  • /:id — The mount prefix is not discarded.
  • /books/books/:id — The app prefix and router path are joined; the prefix is duplicated.

Middleware either continues or answers

JavaScript-style outline · Week 2 order
const logger = (req, res, next) => {
  console.log(req.method, req.path);
  next();
};

booksRouter.use(express.json());
booksRouter.use(logger);
booksRouter.post('/', checkAuth, validateBookParams, handler);

Line by line

  1. const logger = (req, res, next) => {Ordinary middleware takes (req, res, next): the request, the response, and a function that passes control on.
  2. console.log(req.method, req.path);The work this middleware does: print the method and the path.
  3. next();Calling next() hands control to the next applicable handler. Sending a response finishes the request. Doing neither leaves it waiting.
  4. booksRouter.use(express.json());Router-wide middleware applies to matching router traffic. This one parses a JSON body, and it is registered first, so it runs first.
  5. booksRouter.use(logger);Also router-wide; registered second, it runs after the parser.
  6. booksRouter.post('/', checkAuth, validateBookParams, handler);Route-specific middleware applies only to its route. For this POST, checkAuth runs, then validateBookParams, then the handler. Each one is reached only if the one before it called next().

Rebuild it · put the lines in order

Register middleware before the route

Rebuild the Week 2 outline: the logger middleware, then the lines that register the parser, the logger and the POST route on the router. Put the lines in order.

Show the solved code
const logger = (req, res, next) => {
  console.log(req.method, req.path);
  next();
};
booksRouter.use(express.json());
booksRouter.use(logger);
booksRouter.post('/', checkAuth, validateBookParams, handler);

Middleware runs in the order it was registered, so the parser and the logger come before the route. Those two do not depend on each other here, so either may come first.

Python

@booksRouter.before_request
def logger():
    print(request.method, request.path)  # → GET /books/123

JavaScript

const logger = (req, res, next) => {
  console.log(req.method, req.path);
  next();
};

booksRouter.use(logger);

Flask moves on when a before_request function returns None; Express moves on only when you call next().

Middleware runs in the order it was registeredA POST /books request passes through six steps in registration order. Each step either calls next() to continue or sends the response. 1, express.json() parses the JSON body. 2, logger logs the request. 3, checkAuth checks the token. 4, validateBookParams checks title and author. 5, handler, the route, creates the book and answers 201 with the new book. One branch leaves step 3: when the token is wrong, checkAuth answers 403 itself, so steps 4 and 5 never run. A dashed line leaves step 1: malformed JSON makes express.json() call next(err), which skips every step in between and goes to step 6, the error handler, registered last. It answers 500 for every error.Request: POST /books1express.json()parses the JSON bodynext()2loggerlogs the requestnext()3checkAuthchecks the tokennext()4validateBookParamschecks title, authornext()5handlercreates the book403wrong tokensteps 4 and 5never run201new bookmalformed JSONnext(err)skips every step in between6error handlerregistered last500every error
Notice that checkAuth answers 403 itself when the token is wrong, so the two steps below it never run.

An error takes another road: next(err) sends control forward to error middleware.

Week 2’s logger records method, path, and hostname. checkAuth checks the second space-separated Authorization value against SI679 and returns 403 when it fails. It is simpler than HW1’s exact Bearer parser. validateBookParams requires nonblank string title and author, returning 400 otherwise. A valid token plus bad fields reaches validation; bad auth plus bad fields stops at auth.

In short

Middleware either continues with next(), sends a reply, or forwards an error with next(err). Order matters: parse → authenticate → validate → handle. Malformed JSON can fail before authentication.

CHECK · predict before you choose

With valid JSON, bad auth, and bad fields in Week 2, which check stops the request first?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • Field validation: 400 — That later handler is not reached after auth sends a reply.
  • Creation: 201 — Rejected authentication must stop creation.
  • Authentication: 403 — The saved Week 2 chain checks auth before field validation.

The exact Week 2 contract

RouteProtection and inputBehavior in your local code
POST /booksAuth + title/author validation; body title, author, year201 with the new book. id is Date.now(); default status available.
GET /booksPublic; optional title, author, year queries200 with an array; supplied filters are exact matches applied together. Year converts from URL text to a number.
GET /books/:idPublic; convert path id to a number200 with book; 404 when none exists.
PATCH /booksAuth; id in JSON body + at least one changed field400 for unusable id/no changes, 404 when absent, 200 on update. Other fields remain.
DELETE /booksAuth; id in JSON body200 after removal; this implementation also returns 200 when no book matched.
GET /books/badroutePublic teaching routeThrow an error to exercise the final 500 handler. Registered before /:id.

Notice that PATCH and DELETE here use /books and a body id. Later weeks use /products/:id. Learn the endpoint actually being studied. Date.now() is a classroom id generator; two calls in the same millisecond can collide, so it is not a general uniqueness guarantee.

The catalog uses let because reset/removal replaces the array. Removing uses filter to keep every other id. Updating finds the book and uses Object.assign to apply supplied properties. A compile-time Omit does not itself remove forbidden fields from an untrusted runtime object.

In short

Read the exact Week 2 contract: POST creates; GET lists or looks up; PATCH updates supplied fields; DELETE removes. Here PATCH/DELETE use a body id at /books, unlike later /products/:id routes.

CHECK · predict before you choose

For the saved Week 2 PATCH /books, where does the id belong?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • In the JSON body with the changes — Follow this exercise’s actual body-id contract.
  • Only in a /books/:id path — That is a different API design, not the saved Week 2 endpoint.
  • In the response only — The handler needs the target identity before performing the update.

Keep startup separate; put errors last

Simplified teaching outline
// app.ts: define and export the app
app.use('/books', booksRouter);
app.use((err, req, res, next) => {
  res.status(500).send('An unexpected error occurred');
});
export { app };

// index.ts: import app, then start the real listener
app.listen(6790);

Line by line

  1. app.use('/books', booksRouter);Routes are registered first.
  2. app.use((err, req, res, next) => {Express recognizes error middleware by its four parameters: (err, req, res, next). Keep the fourth parameter even when unused. Register it after routes so errors can reach it while Express walks forward.
  3. res.status(500).send('An unexpected error occurred');Answers every error it receives with 500 and a short message.
  4. export { app };Offers the app to other files. This file never starts listening.
  5. app.listen(6790);In index.ts, the only file that starts the real listener, on port 6790.

Rebuild it · put the lines in order

Build the app: parser, routes, errors last

This is app.ts from the Week 4 notes. It follows the same order as the outline above. Put the lines in order so the app is created, parses JSON bodies, mounts the product routes and handles errors.

Show the solved code
import express from 'express';
import { productRouter } from './routes/product-routes.js';
import { errorHandler } from './middleware/error-handler.js';
const app = express();
app.use(express.json());
app.use('/products', productRouter);
app.use(errorHandler);
export { app };

Express runs middleware in the order it was registered: the parser before the routes that read req.body, and the error handler after the routes whose errors it catches.

Python

# app.py: define the app
app.register_blueprint(booksRouter, url_prefix='/books')
@app.errorhandler(500)
def handleError(err):
    return 'An unexpected error occurred', 500  # → 500 An unexpected error occurred

TypeScript

// app.ts: define and export the app
app.use('/books', booksRouter);
app.use((err, req, res, next) => {
  res.status(500).send('An unexpected error occurred');
});

Flask finds an error handler by status code, so its position does not matter.

A 404 handler is ordinary middleware for requests no route handled; an error handler receives errors passed down the chain.

Tests import app.ts, so no fixed-port listener starts. Supertest opens a temporary one; you do not need npm run dev. Because index.ts never runs in tests, check startup by hand once.

In short

app.ts assembles the Express app; index.ts starts it. A 404 fallback handles unmatched requests. Four-argument error middleware goes last and handles forwarded exceptions. Keeping startup separate makes app imports usable in tests.

CHECK · predict before you choose

An error handler is registered before a throwing route. What is the problem?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • A three-argument handler is required — Express identifies error middleware by four parameters.
  • Express walks forward, so the later error cannot reach that handler — Register the error middleware after the routes it must catch.
  • Every exception becomes a normal 404 — Unmatched requests and forwarded errors use different handlers.

The Week 2 test checklist

The tests demonstrate an empty catalog, exact author filtering, rejected missing/wrong tokens, blank-title validation, 201 creation, GET by id/404, PATCH authentication and validation with unchanged fields, and DELETE authentication plus an empty final list. beforeEach calls _resetCatalog() so previous tests’ books do not affect the next one.

Use res.body for parsed JSON and res.text for plain text. Check both status and response content; seeing a plausible book alone does not prove that the endpoint returned the correct code.

What happens to POST /books with no token and a blank title?

403 in Week 2. The auth middleware answers before the title/author validator runs, assuming the submitted text was valid JSON.

In short

Reset the catalog before each test. Test protection, invalid fields, filters, updates, and deletion against the finished contract. A test that passes alone can fail after another test changes shared state.

CHECK · predict before you choose

A filter test passes alone but fails after a create test. What should you check first?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • Remove the filter assertion — Weakening the assertion hides the problem instead of fixing isolation.
  • Always run tests in the lucky order — Tests should start from controlled state rather than depend on order.
  • The beforeEach catalog reset — Leaked state changes the fixture; reset it for every test.

TypeScript setup, headers, and all three NYT blocks

JSONC · configuration illustration
// Important tsconfig.json settings from the lecture:
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "rootDir": "src",
    "outDir": "dist",
    "types": ["node"],
    "strict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

Install Express as a runtime dependency; TypeScript, tsx, @types/node, and @types/express supply development tools/declarations. Vitest, Supertest, and @types/supertest are test dependencies. tsx runs TypeScript during development, but it does not replace tsc --noEmit. Keep generated dist and installed node_modules out of Git with .gitignore.

The request start line contains method/path/protocol, and the response start line contains protocol/status. Both have headers, a blank line, then a body where appropriate. HTTP header names are case-insensitive; Node exposes incoming keys in lowercase. Add My-Header-Field: Woot! in Postman and inspect req.headers['my-header-field']. Compare headers from a browser and Postman; their clients provide different metadata.

The intermediate lecture POST/test initially use 200, then NYT explicitly asks for the appropriate creation status, 201. Your completed Week 2 code and tests use 201. The lecture/local error handler shows a stack trace for learning; HW1 instead sends a generic 500 and logs details on the server.

Course task table: Week 2 Now You Try blocks
Week 2 exercise blockEvery requested task
NYT #1 · filteringImplement exact title, author, and year filters with AND when combined. Create corresponding Postman requests.
NYT #2 · route behaviorChange POST to 201; add GET /books/:id with 404 absence; add authenticated PATCH /books with id + at least one changed field, 200 on success and 400 invalid data.
NYT #3 · testsPOST missing/wrong auth → 403; valid-auth blank title → 400 and text mentioning non-blank; two authors then exact one-author result; DELETE missing auth → 403 and authenticated deletion → empty catalog.

Keep earlier filtering while adding later middleware/tests. Test route behavior against the finished contract, not an intermediate lecture response. BeforeEach reset is necessary even when every test passes by itself.

In short

TypeScript setup compiles the project; HTTP setup chooses the actual method, port, and headers. Keep all three Week 2 activity blocks: filtering, protected route behavior, and tests. Intermediate lecture responses are not the final contract.

CHECK · predict before you choose

The Week 2 lecture first used POST success 200; the finished exercise requests creation status. What should the test expect?

Explain it, then compare the reasoning

Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

  • 204 with a new-book body — 204 means no response body; it is not the chosen creation response.
  • 201 — The finished saved implementation follows the creation contract.
  • 200 forever because it appeared first — An intermediate example can change as the exercise is completed.

Send a request

Build an HTTP request and watch the Week 2 books API handle it one step at a time. Nothing leaves this page: the app, its books and every response are simulated in your browser.

The playground needs JavaScript. Without it, here is one request worked by hand.

HTTP request · the query asks for one author
GET /books?author=Frank%20Herbert HTTP/1.1
Host: localhost:6790
  1. The path starts with /books, so the request enters the books router.
  2. express.json() finds no body to parse and continues.
  3. logger prints the method and path, then continues.
  4. The GET / route keeps the books whose author is exactly Frank Herbert and answers 200.
HTTP response
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 83

[{"id":2,"title":"Dune","author":"Frank Herbert","year":1965,"status":"available"}]

Guided tasks

Work down the list. A task is ticked when a request you send meets it. These ticks are separate from the checks in your practice progress.

    Headers

    Host and Content-Length are added for you, as a real client would.

    Leave it empty to send no body. In JSON, keys and text need double quotes.

    Press Send to see the request, each step and the response.

    5 · Stored books

      Books live only on this page. Reloading the page or pressing Reset books brings back the starting three.

      What this playground decides where the course is silent
      • The catalog starts with three books so there is something to read. The course catalog starts empty.
      • A book’s status is shown as the text available, the value the TypeScript chapter gives that enum member.
      • The 403, the 404 for a missing book, the 400 from PATCH and the 200 from DELETE are sent with res.sendStatus(), so the body is the status name.
      • A successful PATCH answers with the updated book. PATCH accepts only a number as the id.
      • When express.json() leaves req.body undefined, the handlers treat it as an object with no fields.
      • A filter that appears twice in the query arrives as an array and matches no book.
      • The error handler is the short one from this chapter: every forwarded error becomes 500 with the same text and no stack trace. That includes a body that fails to parse.
      • A response shows only Content-Type and Content-Length. Real Express adds a few more headers, such as Date and ETag.
      • Paths are matched the way Express 5 does by default: /Books also reaches the router, and one trailing slash is allowed.

      Source trail · Week 2 course page, books-router.ts, books-service.ts, app/index, tests, and its study plan.

      04

      Homework 1 · menu, role checks, order validation, and errors

      Put the pieces together: SliceDrop.

      Four endpoints; two different roles

      HW1 is an in-memory menu/orders API on port 3000. It combines Week 1 inputs with Week 2 routers, middleware, and tests. It has no MongoDB persistence. Menu data is supplied; stored orders get server-created id, status, and timestamp.

      Where this chapter sitsA client sends a request to the server. It passes route, middleware, controller and service; the service uses the database, and the response goes back to the client. Highlighted in this chapter: all four server stages. The database stays plain because SliceDrop keeps its orders in memory.Clientbrowser, Postman, curlrequestresponseServerRouteMiddlewareControllerServiceDatabasethis chapter
      This chapter lights up the whole server chain, because SliceDrop puts route, middleware, controller and service together and keeps its orders in memory, not in a database.

      Label the map · tap a name, then a slot

      SliceDrop on the map

      Put each name where it belongs on the map.

      The chapter map with its names blankedThe same map as above, with 5 numbered slots where its names were.Clientbrowser, Postman, curl1?responseServer2?3?4?5?Database

      This exercise needs JavaScript. The map above shows every name in its place.

      EndpointWho can call itExpected answer
      GET /menuPublic200; all six items, or exact category filter.
      GET /menu/:idPublic200 with one item; 404 with an error for an unknown id.
      POST /ordersCustomer token201 with created order; 400 with errors for an invalid order.
      GET /ordersStaff token200 with all orders, or exact status filter.

      The menu ids are hawaiian, meat-lovers, build-your-own, bread-nugz, dipping-sauce, and soda. ?category=pizza returns the three pizzas. Unknown category/status filters are valid collection queries with no matches: 200 and []. An unknown single menu id is 404.

      POST /orders in SliceDropPOST /orders in SliceDrop, step by step. express.json() in app.ts parses the body. ordersRouter in routers/orders.ts matches the path. requireCustomerToken in middleware/auth.ts replies 401 for a missing or wrong token. The POST handler calls validateOrder and replies 400 if it returns errors; otherwise addOrder in data/orders.ts stores the order in memory and the reply is 201.401 or 400 replyPOST /ordersexpress.json()app.tsparses the JSON bodyordersRouterrouters/orders.tsmatches POST /ordersrequireCustomerTokenmiddleware/auth.tsno or wrong token: 401POST handlerrouters/orders.tsruns the next two stepsvalidateOrdervalidation/validate-order.tserrors found: the handler replies 400addOrderdata/orders.tspushes the order into memory201 with the created order
      Notice that the token check runs before validation, so a request with no token gets 401 even when its body is invalid.
      HTTP · classroom token from supplied constants
      POST /orders
      Authorization: Bearer slicedrop-customer-secret
      Content-Type: application/json
      
      {
        "customerName": "Ada",
        "items": [{ "menuItemId": "soda", "quantity": 1 }]
      }

      Line by line

      1. POST /ordersThe method and the path: create an order.
      2. Authorization: Bearer slicedrop-customer-secretA header carrying the customer token, after the word Bearer and one space.
      3. Content-Type: application/jsonSays the body below is JSON.
      4. "customerName": "Ada",The customer name. The body carries no id, status, or timestamp; the server creates those.
      5. "items": [{ "menuItemId": "soda", "quantity": 1 }]An array with one item: one soda, named by its menu id.

      Staff listing uses Bearer slicedrop-staff-secret. These strings are the assignment’s demonstration credentials, not a design for production authentication. A valid customer token does not grant staff access.

      In short

      HW1 stores orders in memory. Menu reads are public; customers create orders; staff list them. An empty filtered collection is 200/[]; a missing individual menu item is 404. Roles and response shapes matter.

      CHECK · predict before you choose

      GET /menu?category=unicorn returns no items. Which response fits HW1?

      Explain it, then compare the reasoning

      Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

      • 200 with [] — The collection query succeeded and found zero matching items.
      • 404 because the category is unfamiliar — HW1 treats unknown collection filters as valid queries with no matches.
      • 401 because all menu reads require a staff token — Menu reads are public; staff protection applies to order listing.

      Parsing a credential is different from accepting it

      TypeScript
      export function parseBearerToken(header: string | undefined): string | null {
        if (header === undefined) return null;
        const match = header.match(/^Bearer ([^\s]+)$/);
        return match === null ? null : match[1];
      }

      Line by line

      1. export function parseBearerToken(header: string | undefined): string | null {Takes the Authorization header, which may be missing, and returns the token text or null.
      2. if (header === undefined) return null;No header was sent, so there is no token to read.
      3. const match = header.match(/^Bearer ([^\s]+)$/);Checks the header against a regular expression. The marks ^ at the start and $ at the end anchor the regular expression to the entire value. One literal space follows Bearer. The part [^\s]+ captures one or more non-whitespace characters.
      4. return match === null ? null : match[1];When the header does not fit the pattern, match is null and so is the result. Otherwise the result is the captured part, the token.

      Python

      import re
      def parseBearerToken(header: str | None) -> str | None:
          if header is None: return None
          match = re.fullmatch(r'Bearer ([^\s]+)', header)  # Python's $ would also accept a final \n
          return None if match is None else match[1]
      
      print(parseBearerToken('Bearer nope'))  # → nope
      print(parseBearerToken('Bearer one two'))  # → None

      TypeScript

      export function parseBearerToken(header: string | undefined): string | null {
        if (header === undefined) return null;
        const match = header.match(/^Bearer ([^\s]+)$/);
        return match === null ? null : match[1];
      }
      Header valueParser resultWhat happens next
      Missingnull401
      Bearer slicedrop-customer-secretCandidate token stringCustomer route accepts it; staff route rejects it.
      bearer token, Basic tokennullCase/scheme does not match the exact assignment format.
      Bearer , Bearer one twonullEmpty token or extra whitespace is rejected.
      Bearer nope"nope"Parsing succeeds; role middleware still rejects with 401.

      The two middleware functions read the header, compare the candidate with the constant for their role, and either answer 401 or call next.

      The app’s chain is JSON parser → menu/orders routers → JSON 404 fallback → four-argument error handler. For POST /orders, the router runs customer auth → handler → validateOrder → addOrder. With no token and valid JSON {}, the result is 401 before field validation.

      In short

      Parsing extracts a candidate credential; authorization checks whether it is accepted for this role. HW1 requires exact Bearer syntax and separate customer/staff tokens. A correctly shaped header can still carry the wrong token.

      CHECK · predict before you choose

      Bearer nope has the expected header shape. What follows?

      Explain it, then compare the reasoning

      Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

      • The parser must return the HTTP response itself — The plain parser returns a token or null; middleware chooses the reply.
      • Parsing succeeds; role authentication still rejects it — Extracting a candidate does not prove that it matches an accepted credential.
      • Every protected route accepts it — Authorization must compare the candidate with the role’s expected token.

      Validate structure first; collect every safe-to-find error

      Field / ruleValid conditionReason for the guard
      BodyNon-null, non-array objectA null/array/primitive cannot be treated as an order object.
      customerNameString whose trim() is nonemptyReject absent, wrong-type, and whitespace-only names.
      itemsNonempty arrayCheck Array.isArray before iterating.
      Each itemObject with an existing menuItemIdGuard before accessing menu options.
      quantityInteger at least 1Reject absent, text, zero, negative, and fractional quantities.
      size / crust / sauceIf supplied, correct string and offered by that menu itemAn optional field is still validated when present.
      toppingsIf supplied, array whose every entry is an offered stringA string is not a topping list; unknown/wrong-type entries are errors.

      The handler turns a nonempty errors array into 400 {"errors":[...]}, or creates the order if the array is empty. Keeping the validator plain makes it reusable and easy to test.

      If a menu lookup fails, report the id and quantity issues that can be checked, then skip option checks for that item. Returning inside a forEach callback stops that callback for the current item; it does not return from the outer validator.

      The supplied helpers use some for offered sizes and includes for offered crusts, sauces, and toppings. A missing options list means the option is not offered. The validator must check runtime values even though NewOrder has TypeScript fields.

      JSON-shaped illustration · comments are explanatory, not valid JSON
      // A valid JSON body, but an invalid order:
      {
        "customerName": "   ",
        "items": [{ "menuItemId": "not-on-menu", "quantity": 0 }]
      }
      // With the customer token: 400 and multiple validation errors.
      // Without it: 401 before order validation.

      Line by line

      1. "customerName": " ",Only spaces. After trim() nothing is left, so the validator adds an error.
      2. "items": [{ "menuItemId": "not-on-menu", "quantity": 0 }]One item with two problems. No menu item has this id, and a quantity must be a whole number of at least 1. Each adds an error. Option checks are skipped because the menu lookup failed.
      3. // With the customer token: 400 and multiple validation errors.The handler turns the nonempty errors array into one 400 response that lists every error.
      4. // Without it: 401 before order validation.The auth middleware answers first, so the validator never runs.
      TypeScript · composed for this guide, item checks left out, run with Node 24
      function validateOrder(body: any): string[] {
        const errors: string[] = [];
        if (typeof body?.customerName !== 'string' || body.customerName.trim() === '')
          errors.push('customerName is required');
        if (!Array.isArray(body?.items) || body.items.length === 0)
          return [...errors, 'items must be a nonempty array'];
        return errors;
      }

      Python

      def validateOrder(body) -> list[str]:
          errors, fields = [], body if isinstance(body, dict) else {}
          if not isinstance(fields.get('customerName'), str) or fields['customerName'].strip() == '':
              errors.append('customerName is required')
          if not isinstance(fields.get('items'), list) or len(fields['items']) == 0:
              return [*errors, 'items must be a nonempty array']
          return errors
      print(validateOrder(None))  # → ['customerName is required', 'items must be a nonempty array']

      TypeScript

      function validateOrder(body: any): string[] {
        const errors: string[] = [];
        if (typeof body?.customerName !== 'string' || body.customerName.trim() === '')
          errors.push('customerName is required');
        if (!Array.isArray(body?.items) || body.items.length === 0)
          return [...errors, 'items must be a nonempty array'];
        return errors;
      }

      Python’s .get() needs a dict, hence the isinstance check; body?. works on any value.

      In short

      Check structure before accessing fields: object → items array → each item → offered options. Collect every safely discoverable error. A plain validator returns errors; the handler chooses 400 or creation.

      CHECK · predict before you choose

      items is a string and customerName is blank. What should validation do?

      Explain it, then compare the reasoning

      Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

      • Accept it because NewOrder has declared fields — Runtime data can violate declared types and must be inspected.
      • Report both safe errors and skip item iteration — Check Array.isArray before looping; still collect the name error.
      • Loop first and let a crash become 500 — Invalid client structure should be guarded, not turned into an unexpected fault.

      State, time, and three different failures

      NewOrder describes customer-submitted fields. Order extends NewOrder adds id, status, and createdAt. addOrder creates ids such as "1" and "2", sets status to pending, uses an ISO timestamp, and pushes into the in-memory array. Unfiltered listing preserves insertion order; status filtering keeps exact matches. Restarting Node clears the orders and resets the counter.

      CaseWhere it failsResponse
      Malformed JSON textJSON parser → final handler400; {"error":"Request body must be valid JSON"}
      Valid JSON, invalid order fieldsPlain validateOrder → route handler400; {"errors":[...]}
      Missing / wrong-role / bad tokenRole middleware401; {"error":"Unauthorized"}
      Unknown method/pathFall-through middleware404; {"error":"Not found"}
      Unexpected application exceptionFinal handler500; {"error":"Internal server error"}, with details logged on the server.

      The parser error is recognized as a SyntaxError with a body property. An unexpected exception is not relabeled as the client’s validation error. Test a timestamp’s type and whether Date.parse accepts it rather than asserting an exact clock time.

      In short

      The server creates order id, pending status, and time. Restarting HW1 clears its in-memory orders. Keep failures distinct: malformed JSON, invalid fields, unauthorized access, missing route, and unexpected exception.

      CHECK · predict before you choose

      After restarting HW1, an earlier order disappears. Why?

      Explain it, then compare the reasoning

      Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

      • An ISO timestamp automatically expires the order — A timestamp records time; it does not implement expiration.
      • Orders live in the Node process, not persistent Mongo storage — Restarting loses the process-local array and resets its counter.
      • Every HTTP API deletes all records on restart — Persistence depends on storage; HTTP does not impose this behavior.

      File map and homework test coverage

      File map: HW1 source and test files
      File / groupStudy job
      app.ts / index.tsParser, mount points, fallback, errors; separate listener.
      middleware/auth.tsExact Bearer parsing and distinct customer/staff gates.
      routers/menu.ts / orders.tsTranslate each HTTP request into lookups, validation, and data calls.
      validation/validate-order.tsAccumulate field errors; call offered-option helpers safely.
      data/menu.ts / data/orders.tsSupplied menu lookup; process-local order state and test reset.
      types.ts / constants.tsShared request/stored shapes; exercise tokens and port.
      auth.test.tsMissing/bad/wrong-role tokens; authorized cases; auth before validation.
      menu.test.tsSix items, category filters, customization fields, and missing id.
      orders.test.ts201, unique ids, pending status, parseable time, list/filter/insertion order.
      errors.test.ts404, all validation errors, malformed JSON, and a mocked addOrder failure → 500.

      _resetOrders() runs before each order test. A mock that makes addOrder throw tests the error path without needing a real application fault. Restoring a mock prevents it from affecting later tests. Read the full original homework teaching guide in the source library for its file-by-file walkthrough and 18-question quiz.

      Why is /menu?category=unicorn a 200 while /menu/unicorn is a 404?

      The first asks for a collection filter and correctly gets no matches. The second asks for one particular resource that does not exist.

      In short

      Follow the file boundary: routers translate HTTP; auth gates access; validators collect errors; data modules own state. Tests reset orders and restore mocks so one test cannot contaminate another.

      CHECK · predict before you choose

      A test mocks addOrder to throw. What must happen before later success tests?

      Explain it, then compare the reasoning

      Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

      • Restore the mock and reset the test state — The injected failure and order state must not leak into later tests.
      • Change later tests to expect 500 — That would preserve a test artifact instead of verifying normal behavior.
      • Restart the browser only — The mock is in the test process, not the browser display.

      Source trail · Local HW1 implementation, all four test files, README, and the complete TEACHING_GUIDE.

      05

      Week 3 · MongoDB, BSON, CRUD, ids, and Express integration

      Move state into a database.

      Choose a data model; learn the vocabulary

      Connection to Section 0: MongoDB takes the saved-order-book role. Focus on the difference between a request asking for data and a stored record holding data.

      Where this chapter sitsA client sends a request to the server. It passes route, middleware, controller and service; the service uses the database, and the response goes back to the client. Highlighted in this chapter: the database.Clientbrowser, Postman, curlrequestresponseServerRouteMiddlewareControllerServiceDatabasethis chapter
      This chapter lights up the database: records saved in MongoDB outlast a restart of the server.

      Label the map · tap a name, then a slot

      Where MongoDB sits

      Put each name where it belongs on the map.

      The chapter map with its names blankedThe same map as above, with 5 numbered slots where its names were.1?browser, Postman, curl2?3?Server4?MiddlewareControllerService5?

      This exercise needs JavaScript. The map above shows every name in its place.

      Not installed yet? Set up MongoDB, mongosh and Compass first.

      MongoDB is a document-oriented NoSQL database. SQL databases organize rows into tables and express relationships with keys and joins. They are useful for strongly related data and relational queries. Mongo stores documents in collections, allowing nested objects and arrays and documents with differing fields.

      Database, collection, document, fieldFour boxes nested inside one another, each with the rough SQL word beside it. The outer box is the database week3db; the SQL word is also database. Inside it is the collection products; the SQL word is table. Inside the collection are two documents; the SQL word is row. The first document holds four fields: _id, an ObjectId; name, Duct Tape; price, 5.99; and quantity, 120. The name field is labelled Field; the SQL word is column. The second document is another product, Scotch Tape, with its own fields.Database week3dbSQL: DatabaseCollection productsSQL: TableDocumentSQL: Row_id: new ObjectId('68bf9c…')FieldSQL: Columnname: 'Duct Tape'price: 5.99quantity: 120DocumentSQL: Rowname: 'Scotch Tape', and its own fields
      Notice the nesting: a database holds collections, a collection holds documents, and a document holds fields, each with a rough SQL counterpart.

      The lecture’s three motivations for NoSQL are scalability (distributing data across machines), flexibility (changing document shapes), and simplicity (object-shaped data and queries). These are design tradeoffs, not a rule that NoSQL is always faster or that SQL cannot scale. Flexible storage still needs application contracts, validation, and care when old documents have different shapes.

      MongoDBRough relational comparisonExample
      DatabaseDatabaseweek3db, week3app, week4
      CollectionTableproducts
      DocumentRowOne tape or coffee-maker record
      FieldColumnname, price, quantity

      Mongo stores BSON, a binary representation supporting additional types such as ObjectId and dates. JSON itself supports several value types; it does not make every value a string. Field names are strings, and nested objects/arrays let one document hold structured data. The Node driver translates between JavaScript values and BSON.

      A local Community Edition server is mongod; mongosh is an interactive client, and Compass is a GUI client. The mongodb npm package is a driver, not the server. MongoDB Atlas is a hosted service. Mongo can also support replication/failover and sharding; the class exercise uses one local server.

      In short

      Database → collection → document → fields. MongoDB stores BSON records, including nested values. mongod is the server; mongosh and Compass are clients; the npm driver lets Node communicate with it.

      CHECK · predict before you choose

      You installed the mongodb npm package. Does that start a persistent local database server?

      Explain it, then compare the reasoning

      Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

      • Yes; the driver is the server — A driver connects to a server; it does not replace mongod.
      • Yes; opening Compass always creates the server — Compass is a graphical client and needs a reachable server.
      • No; it is a driver, and mongod must run separately — The client library and database server are different programs.

      Connect, choose a database, then a collection

      mongosh
      // mongosh setup for the exploration database:
      use week3db
      db.createCollection("products")
      show databases
      TypeScript
      import { MongoClient, ObjectId } from 'mongodb';
      import type { Db, Document } from 'mongodb';
      
      const client = new MongoClient('mongodb://127.0.0.1:27017');
      await client.connect();
      const db = client.db('week3db');
      await db.command({ ping: 1 });
      const products = db.collection('products');
      // ...operations...
      await client.close();

      Line by line

      1. import { MongoClient, ObjectId } from 'mongodb';Loads two tools from the mongodb package. The line below it loads two types, which only TypeScript uses.
      2. const client = new MongoClient('mongodb://127.0.0.1:27017');Creates a client for the MongoDB server on your own computer, port 27017.
      3. await client.connect();The first of three distinct steps: connect to the server. The await pauses here until the connection is ready.
      4. const db = client.db('week3db');The second step: select a database by name.
      5. await db.command({ ping: 1 });Checks the connection: it sends the server a ping and waits for the answer.
      6. const products = db.collection('products');The third step: select a collection. The operations below run on it.
      7. await client.close();Closes the client when scratch work or tests finish.

      Python

      from pymongo import MongoClient
      
      client = MongoClient('mongodb://127.0.0.1:27017')
      # no connect() and no await: the client connects by itself
      db = client.get_database('week3db')
      db.command({'ping': 1})  # → {'ok': 1.0}
      products = db.get_collection('products')
      client.close()

      TypeScript

      import { MongoClient, ObjectId } from 'mongodb';
      
      const client = new MongoClient('mongodb://127.0.0.1:27017');
      await client.connect();
      const db = client.db('week3db');
      await db.command({ ping: 1 });
      const products = db.collection('products');
      await client.close();

      A database may not appear in listings until it has stored data/a collection. A long-running app connects at startup and keeps using the connection.

      The exploration program uses week3db; the Express Week 3 app uses week3app. Importing records into one does not populate the other. npm run explore runs the scratch file with watch mode. With insertion left on, every save or restart adds duplicates: seed once, then turn it off.

      In short

      Connect to the server, choose the database, then choose the collection. The database name is part of the destination. Importing into week3db does not populate week3app. Avoid rerunning seed inserts in watch mode.

      CHECK · predict before you choose

      Compass shows records in week3db, but the app reads week3app and gets []. What should you inspect?

      Explain it, then compare the reasoning

      Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

      • Assume find() cannot read imported data — The destination mismatch explains this before blaming the query method.
      • Insert on every file save until the list looks right — Watch-mode inserts can create duplicates; fix the destination and seed deliberately.
      • The app’s selected database and collection — Two databases can hold separate collections with the same name.

      CRUD uses query objects and update operators

      TypeScript
      const products = db.collection('products');
      
      const inserted = await products.insertOne({ name: 'Duct Tape', price: 5.99, quantity: 120 });
      const all = await products.find().toArray();
      const one = await products.findOne({ name: 'Duct Tape' });
      const pricey = await products.find({ price: { $gte: 5 } }).toArray();
      
      const changed = await products.updateOne(
        { _id: inserted.insertedId },
        { $set: { quantity: 150 } }
      );
      const removed = await products.deleteOne({ _id: inserted.insertedId });

      Line by line

      1. const inserted = await products.insertOne({ name: 'Duct Tape', price: 5.99, quantity: 120 });Create. Stores one document. The result reports insertedId, the _id MongoDB generated. It is not a read-back document.
      2. const all = await products.find().toArray();Read everything. Because find() returns a cursor, toArray() is what gives the array.
      3. const one = await products.findOne({ name: 'Duct Tape' });Read one. The object is a query: match documents whose name equals this text. The result is one matching document or null.
      4. const pricey = await products.find({ price: { $gte: 5 } }).toArray();A comparison query. Here $gte means greater than or equal, so this finds every product priced 5 or more.
      5. { _id: inserted.insertedId },An update takes two objects. The first is the query: the document with this id.
      6. { $set: { quantity: 150 } }The second says what to change. Here $set changes quantity and preserves the other fields.
      7. const removed = await products.deleteOne({ _id: inserted.insertedId });Delete. Removes the first document that matches the query. Inspect deletedCount to see whether one was removed.

      Rebuild it · put the lines in order

      Delete one document by its id

      From the Week 3 notes: a function that deletes one product by its id, and the main function that runs it. Put the lines in order.

      Show the solved code
      const deleteOneTest = async () => {
        const query = { _id: new ObjectId('68c06e746d1392af8b22b35f') };
        const result = await db.collection('products').deleteOne(query);
        return result;
      };
      const main = async () => {
        await connect();
        const result = await deleteOneTest();
        console.log(result);
        await disconnect();
      };
      main();

      Build the query with an ObjectId, run the operation, return what it reports, and do it all between connect and disconnect. Printing the result and disconnecting do not depend on each other, so either may come first.

      Python

      inserted = products.insert_one({'name': 'Duct Tape', 'price': 5.99, 'quantity': 120})
      all = list(products.find())
      pricey = list(products.find({'price': {'$gte': 5}}))
      changed = products.update_one(
          {'_id': inserted.inserted_id},
          {'$set': {'quantity': 150}}
      )
      print(changed.modified_count)  # → 1

      TypeScript

      const inserted = await products.insertOne({ name: 'Duct Tape', price: 5.99, quantity: 120 });
      const all = await products.find().toArray();
      const pricey = await products.find({ price: { $gte: 5 } }).toArray();
      const changed = await products.updateOne(
        { _id: inserted.insertedId },
        { $set: { quantity: 150 } }
      );

      pymongo uses snake_case and no await; list() does the job of toArray() on the TypeScript side.

      OperationWhat to know
      insertOne / insertManyCreate one/several documents. Results report acknowledged and insertedId or insertedCount/insertedIds; not a complete read-back document.
      find(query)Returns a Cursor, not the final array. Use await cursor.hasNext()/next() or await cursor.toArray().
      findOne(query)Returns one matching document or null; no-match is not an exception.
      Equality query{name: "Duct Tape"} matches equal field values.
      Comparison query$lt less than, $gte greater than or equal; $gt and $lte are the corresponding other boundaries.
      updateOne / updateManyApply changes to first/all matches. {$set:{price:4.29}} preserves other fields and can add a field.
      matchedCount / modifiedCountA matching record can already have the requested value: matchedCount 1, modifiedCount 0. Use matches to decide existence.
      deleteOne / deleteManyRemove first/all matches; inspect deletedCount. deleteMany({}) removes every document in that collection.
      Other driver optionsreplaceOne replaces a document; {upsert:true} can insert if absent; {$inc:{quantity:1}} increments a field. These are lecture extensions, not required route features.
      TypeScript
      const cursor = db.collection('products').find({ price: { $lt: 5 } });
      const results: Document[] = [];
      while (await cursor.hasNext()) {
        const doc = await cursor.next();
        if (doc) results.push(doc); // next() can be null
      }

      Names can be duplicated. A name filter illustrates queries, but targeting a known id is more precise for a one-resource API. Verify an update by reading afterward; the result object does not contain the changed document.

      In short

      CRUD means create, read, update, delete. find() gives a cursor; toArray() reads its results. $set changes selected fields. matchedCount answers “exists?”; modifiedCount answers “changed?”; deletedCount answers “removed?”

      CHECK · predict before you choose

      Updating a price to its existing value gives matchedCount 1, modifiedCount 0. What does that mean?

      Explain it, then compare the reasoning

      Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

      • The record is missing — matchedCount 1 proves a filter matched a record.
      • The record was deleted — Update counts do not report deletion; use the correct operation/result.
      • The record exists; this update changed nothing — A successful no-op is different from an absent record.

      An id’s shape and its existence are separate questions

      TypeScript
      const id = '507f1f77bcf86cd799439011'; // illustrative shape
      const document = await db.collection('products').findOne({
        _id: new ObjectId(id)
      });

      Line by line

      1. const id = '507f1f77bcf86cd799439011'; // illustrative shapeThe id as the URL carries it: a string of 24 hexadecimal characters.
      2. const document = await db.collection('products').findOne({Asks the products collection for one document.
      3. _id: new ObjectId(id)A query against an ObjectId-valued _id needs new ObjectId(id), because an ordinary string has a different BSON type.

      Python

      from bson import ObjectId  # installed with pymongo
      id = '507f1f77bcf86cd799439011'  # illustrative shape
      document = db.get_collection('products').find_one({
          '_id': ObjectId(id)
      })
      print(document)  # → None

      TypeScript

      const id = '507f1f77bcf86cd799439011'; // illustrative shape
      const document = await db.collection('products').findOne({
        _id: new ObjectId(id)
      });

      A standard Mongo-generated ObjectId is a 12-byte value commonly displayed as 24 hexadecimal characters. Mongo can store other _id types, but these exercises use its generated ObjectIds.

      Reject malformed ids before constructing ObjectId. badID123 should yield 400 in your completed middleware. A syntactically valid id with no matching document yields 404. An id written in the lecture is only an illustration: copy an actual id from your own POST/list/Compass before expecting a successful lookup.

      TypeScript · teaching outline for the Week 3 stretch task, not course code
      export const validateId = (req: Request, res: Response, next: NextFunction) => {
        if (!/^[0-9a-f]{24}$/.test(String(req.params.id))) {
          res.status(400).json({ error: 'Malformed id' });
          return;
        }
        next();
      };

      Line by line

      1. export const validateId = (req: Request, res: Response, next: NextFunction) => {A middleware function: it receives the request, the response, and next.
      2. if (!/^[0-9a-f]{24}$/.test(String(req.params.id))) {Checks the id from the path against a pattern: exactly 24 characters, each a digit or a letter from a to f. The ! at the front makes the branch run when the id does not fit.
      3. res.status(400).json({ error: 'Malformed id' });Answers 400 here. No ObjectId is constructed and Mongo is never asked.
      4. return;Stops, so next() is not called after the response.
      5. next();The shape is fine, so the route runs. Whether a document has this id is a separate question, answered with 404.

      Rebuild it · put the lines in order

      A middleware that answers or continues

      Rebuild validateId, the middleware that checks the shape of an id. Put the lines in order so a malformed id is answered with 400 and a well-formed id continues to the route.

      Show the solved code
      export const validateId = (req: Request, res: Response, next: NextFunction) => {
        if (!/^[0-9a-f]{24}$/.test(String(req.params.id))) {
          res.status(400).json({ error: 'Malformed id' });
          return;
        }
        next();
      };

      A middleware either answers or calls next(), never both: the 400 and its return come first, so next() runs only for an id with the right shape.

      In short

      An ObjectId is commonly shown as 24 hex characters. First check its shape; then query using an ObjectId value. A valid-looking address can still point to no record: malformed → 400; valid but absent → 404.

      CHECK · predict before you choose

      A 24-hex id passes validation, but findOne returns null. Which outcome fits?

      Explain it, then compare the reasoning

      Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

      • 400: every missing document has a malformed id — This id passed the shape check; absence is a separate question.
      • 200 because the id looks valid — A valid representation of an id is not a retrieved document.
      • 404: no matching resource — Shape validation cannot prove existence.

      Wrap Mongo operations with HTTP routes

      db.ts owns connect/disconnect and product operations. It accepts URI/database arguments so the same code can use a practice database or a temporary test database. The router owns request fields and HTTP statuses. app.ts parses JSON and mounts /products. index.ts awaits connection before listening on 6790.

      Week 3 routeDatabase functionLocal success / absence
      GET /productsgetAllProducts → find().toArray()200 with array, including [] when empty.
      GET /products/:idgetProduct → findOne(_id)200 with document; 404 if null.
      POST /productsaddProduct → insertOne()201 with {id}.
      PATCH /products/:idupdateProduct → $set, return matchedCount200 if matched, even for a no-op; 404 if zero matches.
      DELETE /products/:iddeleteProduct → deletedCount200 if deleted; 404 if no match.
      TypeScript · from the Week 3 notes, before validateId is added
      productRouter.get('/:id', async (req: Request, res: Response) => {
        const product = await getProduct(String(req.params.id));
        if (!product) {
          res.sendStatus(404);
          return;
        }
        res.json(product);
      });

      Line by line

      1. productRouter.get('/:id', async (req: Request, res: Response) => {Registers GET for one product. The handler is async because it waits for the database.
      2. const product = await getProduct(String(req.params.id));Reads the id from the path and asks db.ts for that product. The router owns request fields; it does not touch Mongo itself.
      3. if (!product) {When no document has that id, getProduct gives null, and null counts as false.
      4. res.sendStatus(404);The router also owns HTTP statuses: absent means 404. The return on the next line stops the handler.
      5. res.json(product);Found, so send the document as JSON. In Week 3 this raw document still exposes _id.

      GET/PATCH/DELETE id routes first use validateId, so malformed ids receive 400. Week 3’s Product fields are name, price, and quantity. Its raw document responses expose _id; Week 4 adds the mapping to the public id field. ProductUpdate makes fields optional for PATCH. POST builds the known three-field product; PATCH uses supplied changes. Type annotations alone do not enforce runtime body validation.

      _clearProducts() uses deleteMany({}) and is a test helper, not a route. Only call it after deliberately connecting to the isolated test database.

      Rebuild it · put the lines in order

      Connect, then listen

      This is index.ts from the Week 3 notes, the file that starts the server. Put the lines in order so the server only accepts requests once the database is connected.

      Show the solved code
      import { app } from './app.js';
      import { connect } from './db.js';
      const port = 6790;
      await connect('mongodb://127.0.0.1:27017', 'week3app');
      app.listen(port, () => {
        console.log(`Server running on port ${port}`);
      });

      The database is connected before the server listens, so no request can reach a route before its connection exists. The port line and the connect line do not depend on each other, so either may come first.

      In short

      Week 3 routes translate Mongo results into HTTP. Startup connects before listening. PATCH applies supplied fields and checks matches; DELETE checks removals. Week 3 returns raw _id documents; Week 4 adds public id conversion.

      CHECK · predict before you choose

      Week 3 PATCH changes only price. What should happen to name and quantity?

      Explain it, then compare the reasoning

      Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

      • They stay unchanged — The partial update uses $set for supplied fields rather than replacing everything.
      • They are removed automatically — That would violate partial-update behavior.
      • They become empty because every update is a POST — PATCH and POST are different operations; follow the route’s contract.

      All Week 3 Now You Try tasks

      ActivityWhat you must be able to write and verify
      NYT #1 · findPricierTestFind all products with price ≥ 5 using $gte.
      NYT #1 · updatePriceTestSet Scotch Tape’s price to 4.29 with updateOne and $set.
      NYT #1 · deleteByNameTestDelete Masking Tape by name with deleteOne.
      NYT #1 stretch · markOnSaleTestUse updateMany and $lt:5 to add onSale:true to the matching products; inspect differing document fields in Compass.
      NYT #2 · PATCH testsAssert 404 for well-formed absent id; successful update changes only supplied fields and preserves the others.
      NYT #2 · DELETE testsAssert both success 200 and absent id 404; verify the document/list afterward.
      NYT #2 stretch · id middlewareReject malformed ids with 400 and test every affected GET/PATCH/DELETE route.

      The current lecture notes say the MongoMemoryServer section was carried into Week 4. This guide groups the testing ideas together in Chapter 7 while preserving both weeks’ activities here and in the practice lab.

      A PATCH sets price to its existing value. Should matchedCount 1 / modifiedCount 0 become 404?

      No. A matching product exists; the operation is a successful no-op. 404 based only on modifiedCount would confuse “unchanged” with “absent.”

      In short

      Practice boundaries and scope: $gte:5 includes 5; $lt:5 excludes 5. updateOne touches one match; updateMany touches all. Verify changes with a later read, and keep destructive test helpers on the isolated test database.

      CHECK · predict before you choose

      Mark every product cheaper than 5 on sale, preserving other fields. Which operation fits?

      Explain it, then compare the reasoning

      Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

      • updateOne({price:{$gte:5}}, {$set:{onSale:true}}) — This targets one match and the wrong side of the price boundary.
      • deleteMany({price:{$lt:5}}) — Deleting matching products is not marking them on sale.
      • updateMany({price:{$lt:5}}, {$set:{onSale:true}}) — Many applies to all matches; $lt excludes 5; $set preserves other fields.

      Try a query

      Run the Week 3 driver operations on a small products collection, then see what comes back and how the collection changed. Nothing is installed and nothing leaves this page: the collection and every result are simulated in your browser.

      The sandbox needs JavaScript. Without it, here is one query worked by hand.

      TypeScript · the driver call
      await products.find({ price: { $gte: 5 } }).toArray()
      1. The collection holds six products: Duct Tape at 5.99, Scotch Tape at 3.99, Masking Tape at 2.01, Packing Tape at 5, Glue Stick at 1.5 and Stapler at 12.5.
      2. The query object has one condition: price must be greater than or equal to 5.
      3. Three products meet it. Packing Tape counts, because $gte includes a price of exactly 5.
      4. find() gives a cursor, and toArray() reads it into the array below.
      Returned value · as console.log prints it
      [
        {
          _id: new ObjectId('68bf9cae04ffbba55ba70cbf'),
          name: 'Duct Tape',
          price: 5.99,
          quantity: 120
        },
        {
          _id: new ObjectId('68bf9f42d0eabc55610c1ab5'),
          name: 'Packing Tape',
          price: 5,
          quantity: 60
        },
        {
          _id: new ObjectId('68bf9f42d0eabc55610c1ab7'),
          name: 'Stapler',
          price: 12.5,
          quantity: 15
        }
      ]

      Guided tasks

      Work down the list. A task is ticked when an operation you run meets it. These ticks are separate from the checks in your practice progress.

        Write each argument the way the notes do. Keys need no quotes. Text goes in single or double quotes.

        For find and findOne, leave it empty to match every document. To look up one document by id, copy its _id from the collection below.

        Leave _id out and one is generated. insertMany takes several documents inside [ ], separated by commas.

        $set gives fields a new value. $inc adds a number to a field.

        Leave it empty for an ordinary update. With upsert: true, an update that matches nothing inserts a document.

        Press Run to see the driver call, what comes back, why, and the collection afterwards.

        4 · The products collection

          Documents live only on this page. Reloading the page or pressing Reset products brings back the starting six.

          What this sandbox does not do
          • It covers only what the Week 3 notes teach: find, findOne, insertOne, insertMany, updateOne, updateMany, deleteOne and deleteMany; equality, $lt, $lte, $gt and $gte in a query; $set and $inc in an update; and the upsert option.
          • No other operator or option works here: no $or, $in, $ne or $unset, and no regular expressions, sorting, limits, projections, indexes, aggregation or replaceOne. Using one gives a message that names it.
          • find() is shown as the array that toArray() gives. There is no cursor to step through.
          • Comparison operators compare numbers only. A query cannot reach into a nested object or an array, and cannot match a field against one.
          • Every _id is an ObjectId, written as new ObjectId('...') with 24 hexadecimal characters. The real ObjectId constructor also accepts a few other inputs. This one does not.
          • findOne, updateOne and deleteOne take the first match in the order the collection is listed. The notes say only that the first match is whatever Mongo considers it to be.
          • When an operation fails, the sandbox changes nothing. A real insertMany or updateMany that fails partway may already have stored some of its changes.
          • New fields are added in the order you wrote them. A real server may store several new fields in a different order.
          • Returned values are printed the way console.log prints them in the notes. Long or deeply nested values may wrap at different places than they do in Node.
          • Three products, with their ids, prices and quantities, come from the Week 3 notes. The other three are the sandbox’s own. Ids for new documents are generated in your browser.
          • What you type is read by a small parser as data. It is never run as code. It reads objects, arrays, quoted text, numbers, true, false, null and new ObjectId('...'). Keys may go without quotes, // comments are skipped, and curly quotes from a phone keyboard count as plain quotes. Variables, function calls and arithmetic are not read.
          • Documents live only on this page, at most 50 at a time. Nothing is saved and nothing is sent.

          Source trail · Full supplied Week 3 notes; db-explore.ts, db.ts, product-router.ts, validate-id.ts, tests, and study plan.

          06

          Week 4 · REST, representations, and controller–service layers

          Design the public contract.

          REST is an architectural style

          REST means Representational State Transfer, described by Roy Fielding in 2000. Its influence comes from fitting the web’s HTTP model with a consistent client/server interface. The lecture contrasts it with more elaborate distributed-call approaches such as SOAP/CORBA. REST is not simply “an endpoint that returns JSON.”

          Where this chapter sitsA client sends a request to the server. It passes route, middleware, controller and service; the service uses the database, and the response goes back to the client. Highlighted in this chapter: the request and the response, which form the public contract, and the controller and service behind them.Clientbrowser, Postman, curlrequestresponseServerRouteMiddlewareControllerServiceDatabasethis chapter
          This chapter lights up the public contract, the requests and responses a client sees, and the controller and service layers behind it.

          Label the map · tap a name, then a slot

          The layers behind the contract

          Put each name where it belongs on the map.

          The chapter map with its names blankedThe same map as above, with 5 numbered slots where its names were.Clientbrowser, Postman, curlrequestresponseServer1?2?3?4?5?

          This exercise needs JavaScript. The map above shows every name in its place.

          The Week 4 notes list six guiding principles, also called constraints. The table gives each one in the notes’ order, what it means, and where it shows in this course’s API.

          ConstraintWhat it meansIn this course’s API
          Uniform interfaceClients and servers talk in one consistent way: standard HTTP methods used in their standard meaning, and consistent names for endpoints.The lecture’s design gives every resource the same pattern: a plural noun path, with GET to read, POST to create, PATCH to change and DELETE to remove.
          Client–serverThe client and the server are separate programs with separate jobs, so each can change without the other being rewritten.The Express server keeps the products and applies the rules. A client only sends requests and reads responses.
          StatelessEach request carries everything the server needs to complete it. The server keeps no conversation context between requests.A request for one product names its id in the path, and a request that creates one sends the fields in the body. No route needs to remember an earlier request.
          CacheableA response says whether it may be saved and reused later instead of being requested again.The course code sets no caching headers of its own, so its responses do not say. The notes call this constraint hard to do well and not consistently implemented.
          Layered systemThe system is built in layers, and each part sees only the layer next to it.The client sees only the HTTP interface. It cannot tell, and need not know, that a controller, a service and a database layer sit between the route and MongoDB.
          Code on demand · optionalA server may send code for the client to run. This is the only optional constraint.Not used. This API sends data as JSON, never code.

          Stateless does not mean “no database.” Products can persist while each request independently says what it needs. A client receives a representation of the resource’s state, not direct control of the stored document. Editing a client-side object does not update Mongo until another request asks the server to do so.

          In short

          REST organizes a consistent resource interface. A representation is the data a client receives. Stateless means each request supplies its needed context; it does not prohibit stored products or orders. Other constraints still matter.

          CHECK · predict before you choose

          A stateless API stores products in MongoDB. Is that contradictory?

          Explain it, then compare the reasoning

          Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

          • No; durable resources and independent requests can coexist — Stateless concerns request context, not the absence of persistent data.
          • Yes; stateless forbids every database — That confuses conversational state with resource storage.
          • Any JSON API is automatically fully RESTful — JSON alone does not establish the REST constraints.

          Design entities → representations → endpoints

          Begin with the entities and their relationships. Then choose the fields clients see. Then lay out plural resource nouns, using HTTP methods for actions. The database’s storage arrangement should not dictate every public path.

          Representation in the lecture scenarioFields and relationship
          Customerid, firstName, lastName, zipCode. zipCode is text, preserving leading zeros.
          Productid, modelName, modelNumber, manufacturer, color, price, quantity.
          Orderid, customerId, status, items keyed by product id with quantities.
          RelationshipsA customer has orders; each order refers to a customer and contains quantities of products.
          Endpoint designOperations
          /customersGET collection; POST create.
          /customers/:idGET one; PATCH supplied fields; DELETE one.
          /productsGET collection; POST create.
          /products/:idGET one; PATCH supplied fields; DELETE one.
          /customers/:id/ordersGET this customer’s orders; POST a new order for this customer.

          This is a design scenario. The Week 4 exercise implements product GET/list, POST, GET by id, and DELETE stretch; it does not implement the entire customer/order API or PATCH. Scenario policy: decrement stock when an order is submitted, check availability then, and store orders in a top-level collection so they can be queried across customers. Role permissions, searching, payments, and full validation are additional design work, not completed features of this exercise.

          In short

          Design the things and relationships first, then public fields, then method/path pairs. Use resource nouns such as /products. The lecture’s customer/order design is broader than the implemented Week 4 product exercise.

          CHECK · predict before you choose

          You want one customer’s orders in the lecture design. Which path communicates that relationship?

          Explain it, then compare the reasoning

          Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

          • PATCH /products — Changing products is different from reading a customer’s order collection.
          • GET /customers/:id/orders — The nested collection says which customer’s orders are requested.
          • GET /getEverythingAndPay — This action-heavy path does not identify the requested resource relationship.

          Give each layer one responsibility

          The Week 4 layersSix boxes stacked from top to bottom: Routes, Controllers, Services, Models, Database and MongoDB. An arrow labelled Request runs down the left side through Routes, Controllers, Services and Database into MongoDB. A second arrow runs back up the right side from MongoDB through Database, Services and Controllers and leaves as the Response. Routes know which method and path use which controller. Controllers know HTTP: they read req, choose the status and send res. Services know the db and model functions and never see req or res. Models are helpers the service calls; they sit beside the path, not on it. Database knows Mongo operations, not Express, and returns documents with _id included. MongoDB is the database program itself and stores every document with an _id. A ring on the upward arrow marks the Models box: there productFromDocument() turns _id into id, a string, before the service returns the result.RequestResponseRoutesroutes/Know which method and pathuse which controller.Controllerscontrollers/Know HTTP: read req, choose thestatus, send res.Servicesservices/Know the db and model functions.Never see req or res.Databasedb/Knows Mongo operations, not Express.Returns documents, _id included.MongoDBThe database program itself.Stores every document with an _id.Modelsmodels/Helpers the service calls.productFromDocument()turns _id into id, a string.
          Notice that the request passes down one layer at a time, and that _id becomes id at the ring on the way back up, so no layer above ever sees _id.

          The request goes route → controller → service → database; the service uses model functions to convert data on the way back. These are folders in one server, not five network services.

          Build bottom-up: database operations first, then model, service, controller, router, and app/startup. Database code needs no Express. Routes simply connect paths/middleware to controllers.

          TypeScript · from the Week 4 notes
          const getProducts = async (req: Request, res: Response): Promise<void> => {
            const allProducts = await productService.getAll();
            res.json(allProducts);
          };
          
          const getAll = async (): Promise<Product[]> => {
            const productDocs = await db.getAllInCollection(db.PRODUCTS);
            return productDocs.map((pDoc) => productFromDocument(pDoc));
          };

          Python

          def getProducts():
              allProducts = productService.getAll()
              return jsonify(allProducts)
          def getAll():
              productDocs = db.getAllInCollection(db.PRODUCTS)
              return [productFromDocument(pDoc) for pDoc in productDocs]
          print(getProducts().get_json())  # → [{'id': '66f1a2', 'modelName': 'Kettle'}]

          TypeScript

          const getProducts = async (req: Request, res: Response): Promise<void> => {
            const allProducts = await productService.getAll();
            res.json(allProducts);
          };
          const getAll = async (): Promise<Product[]> => {
            const productDocs = await db.getAllInCollection(db.PRODUCTS);
            return productDocs.map((pDoc) => productFromDocument(pDoc));
          };

          A Flask view returns its response instead of calling res.json.

          File map: Week 4 source files
          Week 4 source fileResponsibility
          db/db.tsinit, getAllInCollection, getInCollection, addToCollection, deleteFromCollection; PRODUCTS constant; disconnect/_clearCollection for tests.
          models/product.tsProduct, ProductFields, productFromDocument, productFromFields.
          services/product-service.tsgetAll, get, add, remove; return app-shaped data or absence.
          controllers/product-controllers.tsgetProducts, getProduct, addProduct, deleteProduct; decide 200/201/204/404.
          routes/product-routes.tsMap GET/list, POST, GET/:id, DELETE/:id; register validation middleware.
          middleware/*Id/body validation and final error handling.
          app.ts / index.tsAssemble parser/router/fallback/error chain; await database init before listen.

          In short

          Follow the ticket: route chooses handler → controller handles HTTP → service coordinates work → database queries. Models convert data. These layers are code roles, usually within one program; services should not need req/res.

          CHECK · predict before you choose

          Where should a missing product become an HTTP 404?

          Explain it, then compare the reasoning

          Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

          • The controller — The service returns absence; the HTTP-aware controller chooses the status.
          • The database connection helper — Database code should report data/results without depending on HTTP.
          • The model type declaration — A shape declaration does not send an HTTP reply.

          Trace GET /products/:id all the way through

          Excerpts / condensed outline from your Week 4 code
          // Route
          productRouter.get('/:id', validateId, productControllers.getProduct);
          
          // Controller: HTTP decisions
          const product = await productService.get(String(req.params.id));
          if (!product) {
            res.status(404).json({ error: 'No product with that id' });
            return;
          }
          res.json(product);
          
          // Service: return Product | null, without choosing a status
          const doc = await db.getInCollection(db.PRODUCTS, id);
          return doc ? productFromDocument(doc) : null;
          
          // Database: string -> ObjectId before the query
          return await theDb.collection(collectionName)
            .findOne({ _id: new ObjectId(id) });

          Line by line

          1. productRouter.get('/:id', validateId, productControllers.getProduct);Route. Id validation stops bad shapes before Mongo, then the controller runs.
          2. const product = await productService.get(String(req.params.id));Controller. It reads the path id and asks the service, never Mongo.
          3. if (!product) {The service passed back null, so no product has this id.
          4. res.status(404).json({ error: 'No product with that id' });Here, and only here, absence becomes 404: an HTTP decision, so the controller makes it.
          5. res.json(product);Found, so send the product.
          6. const doc = await db.getInCollection(db.PRODUCTS, id);Service. It asks the database layer for the document.
          7. return doc ? productFromDocument(doc) : null;The service converts an existing document to Product or passes back null. It chooses no status.
          8. .findOne({ _id: new ObjectId(id) });Database. The string becomes an ObjectId for the query. The database returns a document or null.

          Rebuild it · put the lines in order

          GET /products/:id across the layers

          Rebuild the Week 4 outline of GET /products/:id. The four comments stay in place. Put each line under the layer it belongs to, with the controller's lines in the order they run.

          Show the solved code
          // Route
          productRouter.get('/:id', validateId, productControllers.getProduct);
          // Controller: HTTP decisions
          const product = await productService.get(String(req.params.id));
          if (!product) {
            res.status(404).json({ error: 'No product with that id' });
            return;
          }
          res.json(product);
          // Service: return Product | null, without choosing a status
          const doc = await db.getInCollection(db.PRODUCTS, id);
          return doc ? productFromDocument(doc) : null;
          // Database: string -> ObjectId before the query
          return await theDb.collection(collectionName)
            .findOne({ _id: new ObjectId(id) });

          Each layer does one job: the route passes the request on, the controller turns a missing product into 404, the service converts the document, and only the database layer touches Mongo.

          Python

          # Controller: HTTP decisions
          @productRouter.get('/<id>')
          def getProduct(id):
              product = productService.get(str(id))
              if not product:
                  return jsonify(error='No product with that id'), 404  # → 404 {"error":"No product with that id"}
              return jsonify(product)

          TypeScript

          // Controller: HTTP decisions
          const product = await productService.get(String(req.params.id));
          if (!product) {
            res.status(404).json({ error: 'No product with that id' });
            return;
          }
          res.json(product);

          Flask reads the status from the second value a handler returns.

          TypeScript · from the Week 4 notes
          export const productFromDocument = (productDocument: Document): Product => {
            return {
              id: productDocument._id.toString(),
              modelName: productDocument.modelName,
              modelNumber: productDocument.modelNumber,
              manufacturer: productDocument.manufacturer,
              color: productDocument.color,
              price: productDocument.price,
              quantity: productDocument.quantity
            };
          };

          Line by line

          1. export const productFromDocument = (productDocument: Document): Product => {A plain function: a Mongo document in, a Product out. With no database call and no req or res, it can be tested alone.
          2. id: productDocument._id.toString(),The line that matters. The _id, an ObjectId, becomes id, a string. Clients get a string id and no _id.
          3. modelName: productDocument.modelName,This line and the five below it explicitly select the public fields, one at a time.

          The list response comes from the same converter: mapping each document with .map() produces it.

          Trace a request

          Choose a scenario to see where it ends. This is a teaching simulation; it sends no network request.

            In short

            For GET by id: validate shape → query → convert document → reply. The service maps _id to a public id string and returns null for absence. Only the controller chooses the HTTP status.

            CHECK · predict before you choose

            A Week 4 document contains _id plus public fields. What should the client receive?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • Only the HTTP status, never product data — A found GET returns the resource representation as its body.
            • A Product with string id and selected public fields — productFromDocument maps the storage identity and selects the public shape.
            • The raw document with every stored field — Week 4 introduces an explicit public representation instead of leaking storage.

            Follow creation and deletion

            POST follows the same layers. express.json parses req.body, validation checks supplied values, the service prepares the product fields, Mongo inserts the document and generates _id, and the service turns insertedId into an id string. Read back the product with that id to confirm the stored state.

            TypeScript · from the Week 4 notes
            const addProduct = async (req: Request, res: Response): Promise<void> => {
              const postData = req.body;
              const { id } = await productService.add(postData);
              res.status(201).json({ id });
            };

            ProductFields = Partial<Product> makes every field optional. productFromFields supplies empty strings and zero defaults using ??, so a supplied zero stays zero. Its fallback id uses Date.now. That id is a placeholder for an object in memory and must not be stored; Mongo generates the real one. The lecture passes an InsertOneResult into the service, a small deliberate leak of Mongo’s type rather than complete database encapsulation.

            Your local Week 4 service selects known fields and removes the candidate id before insertion; supplied id/_id do not choose the stored identity. It also materializes defaults before storage. The version typed in class inserted the caller’s fields first and applied defaults afterward; the instructor’s post-class memo corrects the lecture code to this same order. Local body middleware rejects arrays/non-objects and wrong supplied field types; price/quantity must be finite nonnegative numbers and quantity an integer. Omitted fields remain allowed.

            The next topic follows that bug step by step.

            For DELETE, the database returns deletedCount and the service reports a boolean. A second DELETE returns 404. The lecture allows either a 200 body or 204 no-body success; your local Week 4 uses 204, whereas Week 3 uses 200.

            TypeScript · composed for this guide, run with Node 24
            const deleteProduct = async (req: Request, res: Response): Promise<void> => {
              res.sendStatus((await productService.remove(String(req.params.id))) ? 204 : 404);
            };

            Python

            @app.post('/products')
            def addProduct():
                created = productService.add(request.get_json())
                return jsonify(id=created['id']), 201  # → 201 {"id":"1"}
            @app.delete('/products/<id>')
            def deleteProduct(id):
                return ('', 204) if productService.remove(id) else ('', 404)  # → 204, then 404 on a second DELETE

            TypeScript

            const addProduct = async (req: Request, res: Response): Promise<void> => {
              const postData = req.body;
              const { id } = await productService.add(postData);
              res.status(201).json({ id });
            };
            const deleteProduct = async (req: Request, res: Response): Promise<void> => {
              res.sendStatus((await productService.remove(String(req.params.id))) ? 204 : 404);
            };

            In short

            POST creates and returns 201/{id}; read back to confirm storage. Week 4 DELETE returns 204 with no body, then 404 if repeated. Local code validates supplied fields and lets Mongo create the stored identity.

            CHECK · predict before you choose

            A Week 4 product is deleted successfully. What should a second DELETE of that same id return?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • 201 with a replacement id — DELETE does not create a replacement resource.
            • 204 with a JSON success body — 204 has no body, and this repeated deletion targets an absent resource.
            • 404 — The resource is now absent; the saved Week 4 contract reports that.

            A bug story: the product that was stored half-filled

            After the Week 4 class, the instructor posted a memo about a bug in the POST /products code the class had typed together. Every function did what its name said. A product could still be stored wrong.

            The story has two problems. The fix for the first one creates the second, so read them in order.

            What the code did, in order

            add() is the service function that creates a product. This is the version the class wrote.

            TypeScript · services/product-service.ts as written in class, from the memo
            const add = async (productInfo: ProductFields): Promise<Product> => {
              const { insertedId } = await db.addToCollection(db.PRODUCTS, productInfo);
              return productFromFields({ ...productInfo, id: insertedId.toString() });
            };
            1. The first line inside the function hands productInfo straight to the database. That value is exactly what the caller sent, so a field the caller left out is still missing.
            2. The second line calls productFromFields(), the model function that fills in a default for each missing field: an empty string for text, zero for a number.

            The order is the bug. The defaults went into the value handed back to the caller. They never went into the stored document.

            What got stored

            The memo tests this with a product that has four fields. It leaves out modelNumber and quantity.

            TypeScript · the request body in the memo’s test
            const partialProduct = {
              modelName: 'My Model',
              manufacturer: 'My Manufacturer',
              color: 'green',
              price: 5.5
            };
            // no modelNumber, no quantity

            With the class version of add(), this is the document that ends up in the database. Mongo generates the _id field.

            Stored document, class version · adapted from the memo’s table, _id value not shown
            {
              _id: ObjectId('...'),
              modelName: 'My Model',
              manufacturer: 'My Manufacturer',
              color: 'green',
              price: 5.5
            }

            The stored document has no modelNumber and no quantity. The database kept a half-filled product.

            Why the response looked fine

            The memo gives three reasons the bug went unnoticed during class.

            • The controller sends back only the id, with res.status(201).json({ id }). Everything productFromFields() filled in was thrown away one line later. The id was the one part that was right.
            • Every product posted in class was complete. With no field missing there is nothing to default, so the bug cannot show.
            • Postman would not have caught it either. You would have to post a partial product, then open the document in Compass and notice a field that is not there.

            First fix: fill in the defaults before the insert

            TypeScript · the corrected order, from the memo
            const product = productFromFields(productInfo);
            const { insertedId } = await db.addToCollection(db.PRODUCTS, product);

            Now productFromFields() runs first, and the filled-in product is what gets inserted. The fields the caller left out are stored with their defaults.

            The placeholder id

            The first fix creates a quieter problem. Look at what productFromFields() does when it is given no id.

            TypeScript · one line of productFromFields(), from the memo
            id: fields.id ?? String(Date.now()),

            It invents an id from the current time. That is fine for an object in memory. But the first fix hands the whole object to Mongo, so the invented id is stored too.

            Stored document after the first fix · adapted from the memo’s table, _id value not shown
            {
              _id: ObjectId('...'),
              id: '1790716131668',
              modelName: 'My Model',
              modelNumber: '',
              manufacturer: 'My Manufacturer',
              color: 'green',
              price: 5.5,
              quantity: 0
            }

            Two ids, one real. Mongo generated _id. The id field is a placeholder that should never have been stored.

            You would never see it through the API. On the way out, productFromDocument() builds the response id from _id and does not read the stored id field. You would see it only by opening the document in Compass.

            Second fix: store the fields, not the id

            The memo shows two ways to write it. The first lists the six fields to store, and id is not in the list.

            TypeScript · corrected add(), from the memo
            const add = async (productInfo: ProductFields): Promise<string> => {
              const product = productFromFields(productInfo);
            
              const { insertedId } = await db.addToCollection(db.PRODUCTS, {
                modelName: product.modelName,
                modelNumber: product.modelNumber,
                manufacturer: product.manufacturer,
                color: product.color,
                price: product.price,
                quantity: product.quantity
              });
            
              return insertedId.toString();
            };

            The second is shorter and stores the same six fields.

            TypeScript · the compact version, from the memo
            const add = async (productInfo: ProductFields): Promise<string> => {
              const product = productFromFields(productInfo);
              const { id, ...productWithoutId } = product;
              const { insertedId } = await db.addToCollection(
                db.PRODUCTS,
                productWithoutId
              );
              return insertedId.toString();
            };

            Read this line one piece at a time: const { id, ...productWithoutId } = product;

            • const declares new names that cannot be reassigned.
            • { } on the left of the equals sign is destructuring: it takes an object apart into variables.
            • id pulls the field named id out into its own variable.
            • ... is the rest element. It collects every field that was not named before it.
            • productWithoutId is the name of the new object that receives the collected fields.
            • = product names the object being taken apart.

            After that line, productWithoutId holds the six product fields. The variable id holds the placeholder and is not used again. The Week 2 PATCH route used the same pattern to separate an id from the other fields.

            The memo says either version is fine. The long one says what it stores. The compact one says what it leaves out.

            Return only the id

            Both corrected versions end with return insertedId.toString(). The declared return type is now Promise<string>, where the class version had Promise<Product>. The controller only needs the id, so add() returns the id as text and nothing else. The controller line loses its braces.

            TypeScript · controllers/product-controllers.ts, from the memo
            const id = await productService.add(postData);   // was: const { id } = ...

            Rebuild it · put the lines in order

            The corrected add()

            Rebuild the corrected add() from the memo, in its compact version. Put the lines in order so the defaults are filled in before anything is stored, and the placeholder id is not stored.

            Show the solved code
            const add = async (productInfo: ProductFields): Promise<string> => {
              const product = productFromFields(productInfo);
              const { id, ...productWithoutId } = product;
              const { insertedId } = await db.addToCollection(
                db.PRODUCTS,
                productWithoutId
              );
              return insertedId.toString();
            };

            Defaults first, then drop the placeholder id, then insert, then return the id: Mongo stores the complete product with one id, its own _id.

            The same product, stored three ways

            Version of add()What Mongo stores
            As written in class_id and the four supplied fields. modelNumber and quantity are absent.
            After the first fixEvery field, plus a placeholder id beside _id.
            After both fixesEvery field, and one id: _id.

            What a test that reads the data back catches

            The memo’s test posts the partial product, then reads the list back through GET /products and finds the new product by its id.

            Vitest + Supertest · the test in the memo
            const created = await request(app).post('/products').send(partialProduct);
            const all = await request(app).get('/products');
            const stored = all.body.find((p) => p.id === created.body.id);
            
            expect(stored.modelName).toBe(partialProduct.modelName); // ← passed
            expect(stored.price).toBe(partialProduct.price);         // ← passed
            expect(stored.modelNumber).toBe('');   // ← failed: got undefined
            expect(stored.quantity).toBe(0);       // ← would fail but never got here

            The first two expectations passed. The third failed: modelNumber came back undefined, not an empty string. That failure is how the first bug was found.

            A test that checked only the POST response would have passed, because the status and the id were both right. Reading the data back is what asked about the fields the caller left out.

            This read-back goes through the API, so it cannot see the placeholder id. That one shows only in the stored document, which is why the memo points to Compass.

            The task changed in class

            The memo also records that the last Now You Try was changed during class, with about fifteen minutes left. The revised task is the test above: verify that a partially specified product sent to POST /products is inserted correctly, with the supplied fields in place and the defaults from productFromFields() for the rest. The Week 4 notes still describe the earlier task, two tests for GET /products/:id.

            In short

            add() stored what the caller sent, then filled in defaults on a value the controller discarded. Fill in defaults first, and keep the placeholder id out of the stored document. The response held only the id, so only reading the data back showed the bug.

            CHECK · predict before you choose

            The class version of add() inserts productInfo first and calls productFromFields() second. A client posts a product with no quantity and gets 201 with an id. What is in the stored document?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • No quantity field at all — The insert ran first and stored exactly what the caller sent. The defaults were applied afterward, to a value that was not stored.
            • quantity: 0, because productFromFields() supplies the default — productFromFields() does supply 0, but it ran after the insert. Its result went back to the caller, not into the database.
            • Every field, plus a second id made from the time — That is the document after the first fix, when the filled-in product is inserted with its placeholder id. In the class version nothing was filled in before the insert.

            Every Week 4 task, including the test-writing anchor

            TaskRequired understanding / evidence
            Lecture · GET /productsImport products.json into week4/products; build all layers; receive two coffee makers with public id fields.
            Lecture · POST /productsSend raw JSON in Postman; receive 201/{id}; list grows to three; Compass confirms the stored document.
            NYT #1 · GET /products/:idAdd db/service/controller/route operations. Model converter already exists. Found id → 200 one object; valid absent id → 404; id string, no _id.
            NYT #1 stretch · DELETECarry delete through the layers; choose a success response; list gets shorter and Compass agrees.
            Lecture · Supertest setupMake init accept URI/db name; start temporary Mongo once, clear/seed before each test, disconnect/stop afterward.
            Lecture · list/create assertionsExactly two seeds, expected manufacturers/order, id shape, no _id; POST then list has three and includes the new manufacturer.
            NYT #2 in the notes · Write the testsdescribe GET /products/:id: 200 with the requested object; 404 for well-formed absent id. Get the existing id from the seeded list; compare objects with toEqual.
            NYT #2 as revised in class · partial product testPost a product with some fields left out, find it in GET /products by the returned id, and assert the supplied values and the defaults. The post-class memo records this change.
            NYT #2 stretch · DELETE testsDelete one; assert success then list length one. Absent id → 404. Action and verification are separate requests.
            Which layer should turn a missing document into HTTP 404?

            The controller. The database/service return absence, and the controller translates it into the HTTP contract.

            In short

            Build product operations through every layer. Test found and absent ids, public fields, creation read-back, and deletion read-back. Derive an existing id from seeded data; use a controlled absent id, not a guessed lecture id.

            CHECK · predict before you choose

            How should a GET-by-id test obtain an existing id reliably?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • Copy any id shown in the lecture slides — An illustrative id may not exist in this test database.
            • Treat every 24-hex string as an existing record — Shape and existence are different checks.
            • Read the known seeded list and use one returned id — The fixture establishes the record; the response gives its actual generated identity.

            Source trail · Full supplied Week 4 notes (including “Write the tests”), the Week 4 post-class memo, local source/test refinements, and Week 4 study plan.

            07

            Weeks 2–4 + HW1 · Vitest, Supertest, database isolation, and matchers

            Make the evidence repeatable.

            Three tools, three jobs

            Where this chapter sitsA client sends a request to the server. It passes route, middleware, controller and service; the service uses the database, and the response goes back to the client. This chapter adds a test box under the server. A test sends requests to the route and checks the responses, as a client would.Clientbrowser, Postman, curlrequestresponseServerRouteMiddlewareControllerServiceDatabaseTestsends requests, reads repliesthis chapter
            This chapter adds the test box: a test sends requests to the server and checks the responses, just as a client would.

            Label the map · tap a name, then a slot

            Where a test stands

            Put each name where it belongs on the map.

            The chapter map with its names blankedThe same map as above, with 5 numbered slots where its names were.Clientbrowser, Postman, curl1?2?Server3?MiddlewareControllerService4?sends requests, reads replies5?

            This exercise needs JavaScript. The map above shows every name in its place.

            ToolJobWhat it does not prove
            VitestRun describe/it tests, hooks, expect matchers, and mocks.A passing suite covers only the assertions it contains.
            SupertestBuild HTTP requests against the exported Express app; inspect status/body/text.Importing app does not execute index.ts startup or test a real fixed-port server.
            MongoMemoryServerLaunch a real temporary mongod on a separate URI for database-backed tests.It is not the persistent local Mongo server required for Compass/manual use.

            A test-only URI/database keeps test data away from the normal week3app/week4 records. MongoMemoryServer is a real database process, not an in-memory JavaScript mock. Its temporary storage behavior depends on the Mongo/storage configuration; isolation comes from connecting your test code to its temporary instance. A random port is not an access-control boundary.

            The first run may download a Mongo binary; later runs use the cached binary. You need not start your normal mongod or npm run dev for these suites, but setup/download errors can still prevent a run. Tests against an ordinary persistent database can accumulate duplicate inserts between runs. A temporary instance solves cross-run leftovers; clear/reset hooks solve state left between tests within one run.

            In short

            Vitest runs tests and assertions. Supertest sends requests to the exported app. MongoMemoryServer supplies a real temporary Mongo process. Think of referee, practice client, and isolated field; none replaces every production check.

            CHECK · predict before you choose

            Supertest passes against an imported app. Which claim remains unproved?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • The tested request received its asserted status — A passing status assertion is evidence for that specific tested request.
            • The test runner executed that passing assertion — That execution is part of the observed passing test, unlike real startup.
            • index.ts starts successfully with the normal database — The tests can supply a separate connection without executing real startup.

            Start once; reset each time; stop once

            Week 4 lifecycle · combined teaching outline
            let mongo: MongoMemoryServer;
            
            beforeAll(async () => {
              mongo = await MongoMemoryServer.create();
              await db.init(mongo.getUri(), 'week4-tests');
            });
            
            beforeEach(async () => {
              await db._clearCollection(db.PRODUCTS);
              for (const product of twoCoffeeMakers) {
                await db.addToCollection(db.PRODUCTS, product);
              }
            });
            
            afterEach(() => {
              vi.restoreAllMocks(); // when a test temporarily replaced behavior
            });
            
            afterAll(async () => {
              await db.disconnect();
              await mongo.stop();
            });

            Line by line

            1. let mongo: MongoMemoryServer;Declared outside the hooks, so beforeAll can fill it and afterAll can use it.
            2. beforeAll(async () => {Runs once, before the first test.
            3. mongo = await MongoMemoryServer.create();Starts a temporary MongoDB. This is the expensive step, which is why it happens once.
            4. await db.init(mongo.getUri(), 'week4-tests');Connects the database code of the app to that temporary server, in a database only the tests use.
            5. beforeEach(async () => {Runs before every test.
            6. await db._clearCollection(db.PRODUCTS);Empties the products collection, so nothing is left from the test before.
            7. for (const product of twoCoffeeMakers) {Inserts the two known coffee makers one at a time, directly through the database. Every test starts from the same two products.
            8. vi.restoreAllMocks(); // when a test temporarily replaced behaviorAfter every test, restores any behavior a test replaced with a mock.
            9. afterAll(async () => {Runs once, after the last test: disconnect, then stop the temporary MongoDB.

            Rebuild it · put the lines in order

            The hooks around every test

            Rebuild the Week 4 test setup. The hook lines stay in place. Put each line in the hook that should run it, in the order it should run.

            Show the solved code
            let mongo: MongoMemoryServer;
            beforeAll(async () => {
              mongo = await MongoMemoryServer.create();
              await db.init(mongo.getUri(), 'week4-tests');
            });
            beforeEach(async () => {
              await db._clearCollection(db.PRODUCTS);
              for (const product of twoCoffeeMakers) {
                await db.addToCollection(db.PRODUCTS, product);
              }
            });
            afterAll(async () => {
              await db.disconnect();
              await mongo.stop();
            });

            Start and connect once, clear then seed before every test, and shut down in reverse at the end: every test starts from the same two products, and nothing is left running.

            Python

            @pytest.fixture(scope='module', autouse=True)
            def database():  # beforeAll, then afterAll
                db.init(mongo.getUri(), 'week4-tests')
                yield
                db.disconnect()
            @pytest.fixture(autouse=True)
            def clean():  # beforeEach
                db._clearCollection(db.PRODUCTS)

            TypeScript

            beforeAll(async () => {
              await db.init(mongo.getUri(), 'week4-tests');
            });
            
            beforeEach(async () => {
              await db._clearCollection(db.PRODUCTS);
            });

            A pytest fixture holds setup and teardown in one function: the code after yield runs where afterAll would.

            The test lifecycleA timeline from top to bottom with two columns: what Vitest runs, and the state of the temporary database. First, beforeAll runs once at the start: the temporary database starts once and is empty. Then, for test 1: beforeEach runs, and the database is cleared, then 2 products are added; test 1 runs and may change the database; afterEach runs and restores mocks. Then, for test 2, the same three steps: beforeEach clears the database and adds the 2 products again, test 2 runs, afterEach runs. The same three steps repeat for every test. Last, afterAll runs once at the end, and the database is stopped.Vitest runsTemporary databasebeforeAllonce, at the startstarts once,emptybeforeEachbefore every testcleared, then2 products addedTest 1one test runsmay be changedafterEachrestores mocksbeforeEachbefore every testcleared, then2 products addedTest 2one test runsmay be changedafterEachrestores mocksThe same three stepsrepeat for every test.afterAllonce, at the endstopped
            Notice that the database starts once and stops once, but is cleared and filled again before every test, so test 2 never sees what test 1 changed.
            HookWhenWhy
            beforeAllOnce before the suite/blockExpensive database start and connection.
            beforeEachBefore every testKnown state: reset or clear + seed.
            afterEachAfter every testRestore mocks or one-test changes.
            afterAllOnce after the suite/blockClose clients and stop the process.

            Week 2 resets the catalog array; HW1 resets orders; Week 3 starts with an empty products collection; Week 4 clears and inserts two known coffee makers. In Week 4, seed the database directly so a broken POST does not masquerade as a broken GET.

            Starting Mongo before every test is expensive. Seeding once lets later tests inherit mutations. Await asynchronous setup and requests so assertions run after the work completes.

            In short

            Start expensive resources once. Before each test, clear and seed known data. Afterward, disconnect and stop what you started. Await setup and requests. Isolation prevents one test’s changes from becoming another test’s surprise.

            CHECK · predict before you choose

            Which hook should restore the two-product fixture before every Week 4 test?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • afterAll only — Cleanup at the end does not prepare the fixture for each test.
            • beforeEach — Each test needs its own known starting state.
            • beforeAll only — One seed at the start lets later tests inherit earlier mutations.

            Choose a matcher that asks the right question

            MatcherQuestionUse / trap
            toBe(200)Is this the same value?Good for primitive statuses/strings/booleans; object identity differs from equal contents.
            toEqual({...})Are the nested contents equal?Objects/arrays; array order matters.
            toStrictEqual({...})Are the contents equal, counting undefined keys and the kind of object?Stricter than toEqual: a key set to undefined counts, an empty slot in an array differs from undefined, and a class instance does not equal a plain object with the same fields. The course’s tests would care when one calls a service function directly and a leftover key set to undefined, such as _id, should fail; a body parsed from JSON never has such a key.
            toHaveLength(2)How many elements/characters?Check array/string size.
            toContain("Braun")Is this value present?Primitive membership/substrings; use toContainEqual for a newly constructed object.
            .not.toContain("REVOTRA")Is this value absent?Pair with positive assertions; an empty wrong list also passes an absence-only test.
            toMatch(/^[0-9a-f]{24}$/)Does the string have the required shape?Generated id: test its format rather than one exact unpredictable value.
            toBeUndefined()Is the property/value undefined?Week 4 public products should not expose _id.
            toBeNull()Is this null?No-match service/database result.
            toBeDefined() / toBeTruthy()Present / truthy?These are broader than checking exact content or identity.
            toThrow()Does a function throw?Pass a function.
            TypeScript · composed for this guide, run with Vitest 5.0
            import { expect, it } from 'vitest';
            const load = async () => { throw new Error('No product with that id'); };
            
            it('asks the right question', async () => {
              const res = { status: 200, body: [{ a: 1 }, { a: 2 }] };
              expect(res.status).toBe(200);
              expect(res.body[0]).toEqual({ a: 1 });
              expect(res.body).toHaveLength(2);
              await expect(load()).rejects.toThrow('No product');
            });

            Python

            def test_asks_the_right_question():
                res = {'status': 200, 'body': [{'a': 1}, {'a': 2}]}
                assert res['status'] == 200
                assert res['body'][0] == {'a': 1}
                assert len(res['body']) == 2
                with pytest.raises(Exception, match='No product'):
                    load()  # → 1 passed

            TypeScript

            it('asks the right question', async () => {
              const res = { status: 200, body: [{ a: 1 }, { a: 2 }] };
              expect(res.status).toBe(200);
              expect(res.body[0]).toEqual({ a: 1 });
              expect(res.body).toHaveLength(2);
              await expect(load()).rejects.toThrow('No product');
            });

            expect({a:1}).toBe({a:1}) fails: two separately created objects. Python’s is asks toBe’s question; == asks toEqual’s. Over HTTP, a throwing route answers 500; rejects.toThrow fits a direct call like load().

            In short

            Ask a precise question: toBe for primitive equality/identity; toEqual for object contents; toHaveLength for counts; toMatch for format. Combine positive and negative assertions: an empty wrong list can pass an absence-only check.

            CHECK · predict before you choose

            Two separately created objects both contain {a:1}. Which matcher compares their contents?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • toHaveLength — That checks a length, not nested object contents.
            • toEqual — Deep content equality does not require the same object identity.
            • toBe — For objects, this asks whether they are the same object reference.

            Worked tests for the Week 4 “Write the tests” task

            Vitest + Supertest · assumes Chapter 7 setup and two seeds
            describe('GET /products/:id', () => {
              it('returns the requested product', async () => {
                const list = await request(app).get('/products');
                expect(list.status).toBe(200);
                expect(list.body).toHaveLength(2);
                const expected = list.body[0];
            
                const res = await request(app).get(`/products/${expected.id}`);
                expect(res.status).toBe(200);
                expect(res.body).toEqual(expected);
                expect(res.body._id).toBeUndefined();
              });
            
              it('returns 404 for an absent, well-formed id', async () => {
                const res = await request(app).get(`/products/${'a'.repeat(24)}`);
                expect(res.status).toBe(404);
              });
            });

            Line by line

            1. describe('GET /products/:id', () => {Groups the tests for one endpoint under a name.
            2. it('returns the requested product', async () => {One test. The text says what should be true, and the function checks it.
            3. const list = await request(app).get('/products');Supertest sends GET /products to the app and waits for the response. No server was started by hand.
            4. expect(list.status).toBe(200);An assertion: the status must be 200.
            5. expect(list.body).toHaveLength(2);The list holds the two seeds and nothing else.
            6. const expected = list.body[0];Keeps the first product, so the test uses a real id from this run.
            7. const res = await request(app).get(`/products/${expected.id}`);Act: ask for that one product by its id.
            8. expect(res.body).toEqual(expected);The product returned alone must have the same contents as the one in the list.
            9. expect(res.body._id).toBeUndefined();The public product must not expose _id.
            10. const res = await request(app).get(`/products/${'a'.repeat(24)}`);The second test asks for a well-formed id that no seed has. The next line expects 404.

            Rebuild it · put the lines in order

            One Supertest test: act, then assert

            Rebuild the first worked test. Put the lines in order: arrange what the test needs, act with one request, then assert on the status before the body.

            Show the solved code
              it('returns the requested product', async () => {
                const list = await request(app).get('/products');
                expect(list.status).toBe(200);
                expect(list.body).toHaveLength(2);
                const expected = list.body[0];
                const res = await request(app).get(`/products/${expected.id}`);
                expect(res.status).toBe(200);
                expect(res.body).toEqual(expected);
                expect(res.body._id).toBeUndefined();
              });

            Arrange, act, assert: each line uses a value from a line above it, and the status is checked before the body. The two body checks do not depend on each other, so either may come first.

            DELETE stretch · action plus independent read-back
            it('deletes one product and proves it is gone', async () => {
              const list = await request(app).get('/products');
              const id = list.body[0].id;
            
              const deleted = await request(app).delete(`/products/${id}`);
              expect(deleted.status).toBe(204); // Week 4 local contract
              expect(deleted.text).toBe('');
            
              const remaining = await request(app).get('/products');
              expect(remaining.body).toHaveLength(1);
              expect(remaining.body.map((p: Product) => p.id)).not.toContain(id);
            
              const missing = await request(app).get(`/products/${id}`);
              expect(missing.status).toBe(404);
            });

            Arrange → act → assert. The hooks arrange known seed data; the request acts; expectations assert the result. For PATCH, compare the changed field and the unchanged fields. For POST, check 201/id then read back. For DELETE, check its response and subsequent absence. A status-only test can miss a server that reports success but never changes data.

            Week 4 had a real case: POST returned 201 and a correct id while the stored product was missing its defaults. Assert on what was stored, not only on the response.

            Week 4’s base list test checks exactly two products, expected manufacturer order, no REVOTRA, ids with 24 hex characters, and no _id. POST adds REVOTRA and grows the list to three. Read the test fixture to understand where those exact counts/names came from.

            In short

            Arrange known data → act with a request → assert its response and stored effect. A successful status alone can hide a write that never happened. POST, PATCH, and DELETE need independent read-back checks.

            CHECK · predict before you choose

            DELETE returns the expected success status. What additional check catches a fake deletion?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • Read afterward and confirm the product is absent — A separate read verifies the state change rather than only the reply.
            • Assert the same status twice without another request — Repeating the same observation adds no evidence about stored state.
            • Check that the response arrived quickly — Speed does not prove the intended record was removed.

            Read a failure; understand a pass

            Start with the failing test’s name and expected/received values. An assertion failure means the run got far enough to compare behavior. A connection/download/import error is a setup problem and may prevent any request from running. A timeout can mean an unawaited operation, waiting for an absent Mongo server, or a request handler that neither answers nor continues.

            Passing tests do not prove that TypeScript compiles, the real startup awaits Mongo, your imported sample data is present, or an instructor has graded the assignment. Run the separate typecheck/build commands and manually start the app when checking the full path. Classroom placeholder checks are not an API regression suite.

            Week 5 continues here: Mock the dependency. Test the rule.

            Why can every Supertest test pass while npm run dev fails?

            The tests import app and supply their own database connection. They never execute index.ts, so wrong startup configuration, an unavailable normal database, or a missing startup await can be invisible to the tests.

            In short

            Read expected versus received first. Setup errors may stop tests before requests run. Passing tests prove only covered assertions; separately check compilation, startup, real data, and the relevant manual flow. Instructor grading is another boundary.

            CHECK · predict before you choose

            Every API test passes. Can you conclude the instructor awarded full marks?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • No; local test evidence and instructor assessment are separate — A passing suite does not establish submission or an external grading result.
            • Yes; passing tests guarantee every rubric item — A suite covers only its own assertions and may miss rubric requirements.
            • Yes; TypeScript types are the official grade — Compilation and course assessment are distinct sources of evidence.

            Source trail · Week 2/HW1 tests, full Week 3/4 testing sections, and both Mongo test setups.

            7+

            Week 5 · layers, one PATCH request, test kinds, Vitest, and mocks

            Mock the dependency. Test the rule.

            Four layers, four jobs

            Week 5 starts from the finished Week 4 products API, cleaned up and given a few new names. One job moved: turning a MongoDB document into a Product now happens in the repository, not in the model. That leaves the service with nothing but Products to handle, which is what makes it testable on its own.

            Where this chapter sitsA client sends a request to the server. It passes route, middleware, controller and service; the service uses the database, and the response goes back to the client. Highlighted in this chapter: the service, tested for real, and a dashed ring labelled mock around the repository and the database below it, which a unit test replaces.Clientbrowser, Postman, curlrequestresponseServerRouteMiddlewareControllerServiceDatabasemockrepositoryand databasethis chapter
            This chapter tests the service for real and replaces the repository and the database below it with a mock.

            Label the map · tap a name, then a slot

            What a unit test replaces

            Put each name where it belongs on the map.

            The chapter map with its names blankedThe same map as above, with 5 numbered slots where its names were.Clientbrowser, Postman, curlrequestresponseServer1?2?3?4?5?

            This exercise needs JavaScript. The map above shows every name in its place.

            The starter’s src folder, as the notes list it:

            src/ · from the Week 5 notes
            src/
            ├── db/            db.ts, products-repository.ts
            ├── models/        product.ts
            ├── services/      products-service.ts
            ├── controllers/   products-controller.ts
            ├── routes/        products-router.ts
            ├── middleware/    error-handler.ts
            ├── __tests__/     products.e2e.test.ts, test-products.ts
            ├── errors.ts
            ├── mocks-and-spies.unit.test.ts
            ├── app.ts
            └── index.ts
            PartIts jobKnows about
            routes/products-router.tsSends each method and URL to one controller function.HTTP method and path
            controllers/products-controller.tsReads the request, calls the service, chooses the status, sends the response.HTTP: req and res
            services/products-service.tsApplies the business rules, such as “quantity can never be negative”. Products in, Products out.Products only
            db/products-repository.tsReads and writes MongoDB and turns documents into Products and back. db.ts holds the connection.MongoDB and Products
            models/product.tsDeclares the shared types: Product, NewProduct, ProductFields.Types only
            errors.ts, middleware/error-handler.tsDefines ValidationError and turns a thrown error into a 400 or 500 response.Errors and HTTP
            app.tsBuilds the Express app and exports it.Express
            index.tsConnects to MongoDB, then starts listening.Startup
            __tests__/The end-to-end tests and their seed products.The whole app

            The notes correct one Week 4 idea. The lecture said the service translates between the database and the application. It is more common for the repository to do it, so the course now does: the repository’s toProduct turns _id into id, and nothing above the repository ever sees an ObjectId or a document.

            You seeIt belongs in
            req, res, res.status(404)The controller, or the error middleware. Nothing below the controller knows HTTP.
            “price cannot be negative”The service. It is a business rule; HTTP, TypeScript and MongoDB do not know it.
            ObjectId, Document, $setThe repository. MongoDB details stay in one layer.
            The fields of a Product and their typesThe model. Every layer works from one definition.

            app.ts and index.ts are two files for the tests’ sake. app.ts builds the app and exports it; index.ts imports it, connects to the database and calls listen. Supertest imports the app from app.ts, so a test gets every route without starting the server or touching your normal database. The test connects to its own temporary MongoDB first.

            In short

            Routes pick the controller, controllers speak HTTP, services hold the business rules, and the repository speaks MongoDB and turns documents into Products. Models are only types. app.ts builds the app for tests; index.ts connects and listens.

            CHECK · predict before you choose

            A new helper builds MongoDB’s $set update from the changed fields. Which layer should it live in?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • The service, because it knows which fields changed — The service decides what may change, but $set is a MongoDB detail it should never see.
            • The repository, because $set is a MongoDB detail — MongoDB operators stay in the layer that talks to MongoDB, so the layers above handle only Products.
            • The controller, because the fields came from the request body — The controller speaks HTTP; it hands the fields to the service and never builds database updates.

            Follow one PATCH request

            The notes follow one request down to MongoDB and back up: a PATCH to /products/:id with a body such as {"price": 12.5}. At each layer, ask what that layer is responsible for.

            One PATCH request, down and backA PATCH /products/:id request with the body {"price": 12.5} goes down from the client through the router (productsRouter.patch), the controller (patchProduct), the service (updateProduct) and the repository to MongoDB. The stored document comes back up: the repository turns it into a Product and returns it to the service. Three outcomes branch at the service. A Product: the controller sends 200 with the product. null, when no product has that id: the controller sends 404. A ValidationError, when the price or quantity would go negative: the error handler sends 400 and nothing is written. That answer goes back to the client as the response.ClientPATCH /products/:id{"price": 12.5}requestRouterproductsRouter.patchControllerpatchProductServiceupdateProductRepositoryfinds, then writesMongoDBproducts collectiondocumenta Producta Product200 + the productcontrollernull404 · not foundcontrollerValidationError400 · not savederror handlerresponse
            Notice that all three answers start at the service: what it returns or throws decides whether the client gets 200, 404 or 400.

            Router

            routes/products-router.ts · from the Week 5 notes
            productsRouter.patch('/:id', productsController.patchProduct);

            The router is mounted at /products, so this line sends PATCH /products/:id to patchProduct and to nothing else.

            Controller

            controllers/products-controller.ts · from the Week 5 notes
            export const patchProduct = async (
              req: Request,
              res: Response
            ): Promise<void> => {
              const product = await productsService.updateProduct(
                String(req.params.id),
                req.body
              );
              if (!product) {
                res.status(404).json({ error: 'No product with that id' });
                return;
              }
              res.json(product);
            };

            The controller is the only layer that knows HTTP. It turns the service’s answer into a status: null becomes 404 with an error message, and a Product becomes 200 with the product, since res.json sends 200 unless told otherwise. If the service throws, the rest of the function never runs, and Express passes the error to the error handler.

            Service

            services/products-service.ts · from the Week 5 notes
            export const updateProduct = async (
              id: string,
              changes: ProductFields
            ): Promise<Product | null> => {
              const existing = await productsRepository.findProduct(id);
              if (!existing) {
                return null;
              }
            
              const quantity = changes.quantity ?? existing.quantity;
              const price = changes.price ?? existing.price;
            
              if (quantity < 0) {
                throw new ValidationError('quantity cannot be negative');
              }
              if (price < 0) {
                throw new ValidationError('price cannot be negative');
              }
            
              return productsRepository.updateProduct(id, changes);
            };

            Read it as four steps in a fixed order:

            1. Find: ask the repository for the stored product. None means return null and stop.
            2. Compute: work out the quantity and price the product would end up with.
            3. Validate: a negative result throws a ValidationError.
            4. Write: only now ask the repository to save the changes.

            Because the write comes last, a rejected change never reaches the database.

            changes.quantity ?? existing.quantity means: the new quantity if the request sent one, otherwise the stored one. ?? falls back only when the left side is null or undefined. That matters for zero:

            JavaScript · run with Node 20
            console.log(0 ?? 10);          // → 0
            console.log(undefined ?? 10);  // → 10
            console.log(null ?? 10);       // → 10
            console.log(0 || 10);          // → 10

            With ||, a request that sets the quantity to 0 would be checked as if it kept the old quantity. Whether 0 itself is allowed is a business decision. This service allows it, because both checks reject only values below 0.

            Repository

            db/products-repository.ts · from the Week 5 notes
            export const updateProduct = async (
              id: string,
              changes: ProductFields
            ): Promise<Product | null> => {
              const doc = await getDb()
                .collection(PRODUCTS)
                .findOneAndUpdate(
                  { _id: new ObjectId(id) },
                  { $set: changes },
                  { returnDocument: 'after' }
                );
              return doc ? toProduct(doc) : null;
            };
            PartWhat it does
            { _id: new ObjectId(id) }Which document: the one with this id. The id string becomes an ObjectId here, in the repository.
            { $set: changes }Change only the fields that were sent and leave the rest as they are.
            { returnDocument: 'after' }Return the document as it is after the update, not before.
            doc ? toProduct(doc) : nullNo document means null. Otherwise toProduct turns it into a Product, and _id becomes the string id.

            What each layer promises, from the notes:

            LayerPromise
            RouterPATCH /products/:id always reaches the controller’s patchProduct function.
            Controller404 when the id is missing, otherwise 200 and the updated product.
            ServiceQuantity and price never go negative, and nothing is written if they would.
            RepositoryOnly the fields that were sent change, and the change is saved.
            ModelEvery Product has the same shape, with id and never _id.

            In short

            A PATCH goes router, controller, service, repository, MongoDB, and the Product comes back the same way. The service finds the product, works out the new values with ??, validates them, and writes only if they pass. The controller turns the answer into 200 or 404.

            CHECK · predict before you choose

            A client sends PATCH with quantity 0 to a product whose stored quantity is 10. Which quantity does the service validate?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • 0, because ?? only replaces null or undefined — 0 is a real value, so ?? keeps it, and 0 passes because only values below 0 are rejected.
            • 10, because 0 counts as missing — That is what || would do. ?? treats 0 as a value the client sent.
            • None, because zero skips validation — Every computed value goes through both checks; zero simply passes them.

            Missing, invalid, and broken are three outcomes

            A PATCH request can end three ways, and the code keeps them apart on purpose.

            SituationThe serviceWho answersResponse
            No product has that idReturns nullController404 { error: 'No product with that id' }
            The price or quantity would go negativeThrows a ValidationErrorError handler400 { error: 'price cannot be negative' }
            Anything unexpected, such as the database being downLets the error pass throughError handler500 { error: 'Something went wrong' }

            ValidationError is the app’s own error type, one line in errors.ts:

            errors.ts · from the Week 5 notes
            export class ValidationError extends Error {}

            It adds nothing to Error. Its only job is to be recognisable: the error handler asks err instanceof ValidationError, which is true only for an error created with new ValidationError and false for any other error.

            middleware/error-handler.ts · from the Week 5 notes
            import type { NextFunction, Request, Response } from 'express';
            import { ValidationError } from '../errors.js';
            
            export const errorHandler = (
              err: Error,
              req: Request,
              res: Response,
              next: NextFunction
            ): void => {
              if (err instanceof ValidationError) {
                res.status(400).json({ error: err.message });
                return;
              }
              res.status(500).json({ error: 'Something went wrong' });
            };

            Earlier weeks answered every error with 500. Here one kind of error means the client sent bad data, so it gets 400 with the message the service wrote. Everything else still gets 500 with one fixed message. The alternative is a try/catch in every controller, repeating the same handling in each. The notes say HW2 takes a different approach, so read its error handling before assuming this one.

            Two bad ids are not the same problem. 'a'.repeat(24) is 24 hexadecimal characters: a valid ObjectId that no seed product has. It reaches MongoDB, matches nothing, and comes back as null, so the answer is 404. A malformed id such as abc, fails earlier: new ObjectId(id) in the repository throws. Unless something checks ids first, that error is not a ValidationError, so the error handler answers 500. A test that uses a 24-character id checks the missing-product case only. Week 3 drew the same line between an id’s shape and its existence.

            In short

            Missing: the service returns null and the controller sends 404. Invalid: the service throws a ValidationError and the error handler sends 400. Anything else: the error handler sends 500. A 24-character id that matches nothing is a 404, not a bad id.

            CHECK · predict before you choose

            A PATCH asks to set the price of an existing product to -5. What sends the response, with which status?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • The controller, with 404 — 404 is for an id that matches nothing; this product exists.
            • The service, with res.status(400) — The service never touches res; it throws and lets the error handler answer.
            • The error handler, with 400 — The service throws a ValidationError, and the error handler maps that type to 400.

            Unit, integration, end-to-end, regression

            Week 5 adds unit tests beside the end-to-end tests from Week 4. Each name says how much of the app runs for real.

            KindRuns for realReplacedIn this API
            UnitOne functionWhat it calls, with test doublesThe real updateProduct in the service, with the repository functions replaced
            IntegrationA few parts togetherWhat sits outside that groupThe real service and controller with the repository mocked, or the real repository against a test database
            End-to-endThe whole app, through HTTPOnly the database, by a temporary oneSupertest sends a PATCH, then a GET reads the product back
            RegressionAny of the aboveDepends on the testRunning every test that passed before, after each change

            Each kind answers a different question. End-to-end: does the connected API keep its promise? Unit: given these answers from what it calls, does this function decide correctly? A failing end-to-end test says something between the request and the database is broken, not where; the notes’ example is a low-stock test that could fail in the controller, the service, the repository or the seed data. A passing unit test says nothing about whether the layers are wired together. You need both.

            Regression is not a size. It is a reason to run tests: every time anything changes, run every test that passed before, so a change in one place cannot quietly break another. Unit and end-to-end tests both belong in it.

            A unit test replaces what the function calls with a test double, a stand-in the test controls. The notes use three words. A fake, made with vi.fn(), stands in for a function and remembers every call. A mock is a fake that answers with a value you chose. A spy, made with vi.spyOn, wraps a real function and records how it was called; on its own it still runs the real code. For a service, the doubles stand in for the repository layer.

            In short

            A unit test runs one function and replaces what it calls with test doubles. An integration test runs a few parts together. An end-to-end test sends real HTTP through the whole app. Regression means rerunning all of them after every change: a purpose, not a size.

            CHECK · predict before you choose

            Every unit test for the service passes. Which question can they not answer?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • Does the service reject a negative price? — That is exactly the kind of decision a service unit test checks.
            • Does a PATCH request reach the service and save the change? — Unit tests replace the router, controller and repository around the service, so only an end-to-end test shows the parts connected.
            • Does the service pass only the sent fields to the repository? — A spy on the repository function records its arguments, so a unit test can check this.

            Vitest, Supertest, MongoMemoryServer

            Three tools share the work. None is new this week; what is new is running two suites separately.

            ToolIts job hereWatch for
            VitestFinds and runs the tests, and provides expect, the hooks, vi.fn and vi.spyOn.It runs both suites.
            SupertestSends requests to the app that app.ts exports and hands back the response.No port and no npm run dev needed.
            MongoMemoryServerStarts a real, temporary MongoDB for the end-to-end tests and stops it after.The first run downloads MongoDB, so it is slow once.

            After you clone the NYT repository, the notes start with:

            Terminal · from the Week 5 notes
            npm install
            npm test

            The test scripts in package.json, from the notes:

            package.json · from the Week 5 notes
            "test": "vitest run",
            "test:unit": "vitest run unit.test --passWithNoTests",
            "test:e2e": "vitest run e2e.test"

            npm test runs every test. The word after vitest run is a filter on the file name, so the naming convention is what makes the two scripts work: a unit test sits next to the code it tests and its name ends in .unit.test.ts, and an end-to-end test sits in __tests__/ and its name ends in .e2e.test.ts.

            --passWithNoTests: Vitest normally fails when no test file matches. This flag lets npm run test:unit pass with zero tests, so a green run can mean nothing ran. Read the number of tests in the output, not only the color.

            it.todo marks a test still to write: Vitest lists it as todo, not as a failure. The starter shows four. The Now You Try tasks replace them with real tests, and you are done when no todo and no failure is left.

            In short

            Vitest runs the tests, Supertest sends requests to the imported app, and MongoMemoryServer gives end-to-end tests a temporary real MongoDB. test:unit and test:e2e pick files by name. --passWithNoTests lets an empty run pass, so read the count. it.todo marks a test still to write.

            CHECK · predict before you choose

            You rename products-service.unit.test.ts to products-service.test.ts, and npm run test:unit is still green. What happened?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • Vitest found the file under its new name and ran it — The unit script only runs files whose name contains unit.test.
            • The tests in the renamed file all passed — They never ran under test:unit, so this run says nothing about them.
            • The filter skipped the file, so its tests did not run — The script filters on unit.test in the file name; the other unit test file still runs and passes, so the run stays green.

            Arrange, act, assert

            This is the happy-path PATCH test from the notes. It is already in the end-to-end test file:

            __tests__/products.e2e.test.ts · from the Week 5 notes
            it('updates the field that was sent', async () => {
              const all = await request(app).get('/products');
              const { id } = all.body[0];
            
              const res = await request(app)
                .patch(`/products/${id}`)
                .send({ price: 12.5 });
            
              expect(res.status).toBe(200);
              expect(res.body.price).toBe(12.5);
            });
            1. Arrange: create the conditions. The test reads the list and takes the first product’s id. It trusts GET here; other tests check GET.
            2. Act: do the one thing under test. Send the PATCH and keep the response.
            3. Assert: check what should now be true. The status is 200 and the body has the new price.

            Every request is awaited, so each expect reads a finished response.

            Pick a new value that no seed product already has. The notes ask you to check that no product in test-products.ts starts at 12.5. If the stored price were already 12.5, a PATCH that changed nothing would still pass.

            The same test in pytest, with the three parts marked:

            Python

            def test_updates_the_field_that_was_sent(client):
                # Arrange
                all_products = client.get('/products')
                product_id = all_products.json()[0]['id']
                # Act
                res = client.patch(f'/products/{product_id}', json={'price': 12.5})
                # Assert
                assert res.status_code == 200
                assert res.json()['price'] == 12.5

            TypeScript

            it('updates the field that was sent', async () => {
              // Arrange
              const all = await request(app).get('/products');
              const { id } = all.body[0];
              // Act
              const res = await request(app)
                .patch(`/products/${id}`)
                .send({ price: 12.5 });
              // Assert
              expect(res.status).toBe(200);
              expect(res.body.price).toBe(12.5);
            });

            pytest runs each function whose name starts with test_, and a failing assert fails the test. Vitest runs each it() and fails on a failing expect. The Python side assumes a test client named client that works like FastAPI’s TestClient; run with that client under pytest 9.1, it passed.

            MatcherAsksExample
            toBeIs it the same primitive value?expect(res.status).toBe(200)
            toEqualDo objects or arrays have the same contents?expect(names).toEqual(['Few', 'None'])
            toHaveLengthHow many items?expect(low).toHaveLength(0)
            toBeNullIs it null?A lookup that found nothing
            toBeUndefinedIs it undefined?expect(res.body._id).toBeUndefined()
            toThrowDoes calling this function throw?expect(() => fn()).toThrow(): pass a function, not its result
            rejects.toThrowDoes this promise reject?await expect(promise).rejects.toThrow(ValidationError)

            expect({ price: 12.5 }).toBe({ price: 12.5 }) fails: two objects made separately are not the same object. toEqual passes.

            The full matcher table is in the tests chapter.

            In short

            Arrange the data, act once, assert on what changed. Use a value that no seed already has, or a PATCH that changes nothing still passes. toBe compares primitive values, toEqual compares contents, and a rejected promise is checked with await expect(...).rejects.toThrow().

            CHECK · predict before you choose

            A PATCH test sends price 20, and the seeded product already costs 20. The service is broken: it returns the stored product without writing. What does the test report?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • It passes, because the body still shows 20 — The broken service hands back the stored product, whose price is already 20, so the assertion cannot tell.
            • It fails, because the write never happened — Nothing in the test checks for a write; it only compares the price in the body.
            • It fails, because toBe cannot compare numbers — toBe compares numbers fine; 20 is the same value as 20.

            Start once, reset each test, stop once

            The end-to-end file needs a real database, so it starts a temporary MongoDB once, connects to it, and stops it once at the end:

            __tests__/products.e2e.test.ts · from the Week 5 notes
            let mongo: MongoMemoryServer;
            
            beforeAll(async () => {
              mongo = await MongoMemoryServer.create();
              await connect(mongo.getUri(), 'test');
            });
            
            afterAll(async () => {
              await disconnect();
              await mongo.stop();
            });

            Before every test, it empties the products and inserts the seed products again:

            __tests__/products.e2e.test.ts · from the Week 5 notes
            beforeEach(async () => {
              await productsRepository._clearProducts();
              for (const product of testProducts) {
                await productsRepository.insertProduct(product);
              }
            });

            Starting MongoDB is slow, so it happens once. Resetting is quick, so it happens every time. Without the reset, a test that changes a price leaves it changed for the next one, and a result depends on which test ran first: it passes alone and fails with the others. The connect call in beforeAll points the app at the temporary database before any clearing runs, so _clearProducts never reaches your normal data.

            Chapter 7 reads the same three hooks line by line.

            In short

            beforeAll starts the temporary MongoDB and connects once, beforeEach clears and reseeds the products before every test, and afterAll disconnects and stops it. Without the reset, tests depend on their order.

            CHECK · predict before you choose

            Someone moves the clear-and-reseed code from beforeEach into beforeAll. What changes?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • Nothing, because the data is seeded either way — It is seeded once, then every test sees what the tests before it changed.
            • The suite gets slower, because seeding runs more often — It runs less often, once instead of before every test.
            • A test that edits a product can break a later test — With no reset between tests, a changed price or a deleted product carries over, so results depend on order.

            Six PATCH tests and what each proves

            Each test is evidence for one promise and silent about the rest. Read the last column before trusting a green run.

            TestSendsProvesDoes not prove
            Updates the field that was sent{ price: 12.5 }200 and the new price in the bodyThat the other fields stayed, or that anything was saved
            Rejects a negative quantity{ quantity: -1 }400 and the quantity messageThat nothing was written
            404 for a missing id'a'.repeat(24)A well-formed id that matches nothing gets 404What happens to a malformed id
            A · rejects a negative price{ price: -1 }400 and the price messageThat nothing was written
            C · leaves the other fields aloneThe old price plus 1Every unsent field kept, and the change savedAnything about a rejected change
            D · unchanged after a rejected changeA bad quantity and a good priceNothing was saved, not even the good priceWhich layer refused it

            The first test is in arrange, act, assert. The next two are in the notes:

            __tests__/products.e2e.test.ts · from the Week 5 notes
            it('rejects a change that would make quantity negative', async () => {
              const all = await request(app).get('/products');
              const { id } = all.body[0];
            
              const res = await request(app)
                .patch(`/products/${id}`)
                .send({ quantity: -1 });
            
              expect(res.status).toBe(400);
              expect(res.body.error).toBe('quantity cannot be negative');
            });
            __tests__/products.e2e.test.ts · from the Week 5 notes
            it('404s when the id is not in the database', async () => {
              const res = await request(app)
                .patch(`/products/${'a'.repeat(24)}`)
                .send({ price: 1.5 });
            
              expect(res.status).toBe(404);
            });

            The 404 test needs no arranging. 'a'.repeat(24) is a valid ObjectId that no seed product has, so it reaches MongoDB and comes back empty. You can also assert the body: expect(res.body.error).toBe('No product with that id');

            Now You Try #1 asks for three more. The notes do not print their solutions, so these are composed here. A copies the negative-quantity test and changes two values:

            TypeScript · composed for this guide, run with Vitest 5.0 against the Week 5 starter
            it('rejects a change that would make the price negative', async () => {
              const all = await request(app).get('/products');
              const { id } = all.body[0];
            
              const res = await request(app)
                .patch(`/products/${id}`)
                .send({ price: -1 });
            
              expect(res.status).toBe(400);
              expect(res.body.error).toBe('price cannot be negative');
            });

            C checks every field at once. Spreading the original and replacing one field builds the exact product you expect, so toEqual fails if any unsent field moved. The second request reads the product back, which proves the change was saved, not only reported.

            TypeScript · composed for this guide, run with Vitest 5.0 against the Week 5 starter
            it('leaves the other fields alone', async () => {
              const all = await request(app).get('/products');
              const original = all.body[0];
              const newPrice = original.price + 1;
              const expected = { ...original, price: newPrice };
            
              const res = await request(app)
                .patch(`/products/${original.id}`)
                .send({ price: newPrice });
              expect(res.status).toBe(200);
              expect(res.body).toEqual(expected);
            
              const after = await request(app).get(`/products/${original.id}`);
              expect(after.body).toEqual(expected);
            });

            D asks a different question: after a refusal, is the stored product untouched? It sends a valid price beside the invalid quantity. If the service wrote the price before checking the quantity, the response would still be 400, and only the read-back would catch it.

            TypeScript · composed for this guide, run with Vitest 5.0 against the Week 5 starter
            it('leaves the product unchanged after a rejected change', async () => {
              const all = await request(app).get('/products');
              const original = all.body[0];
            
              const res = await request(app)
                .patch(`/products/${original.id}`)
                .send({ quantity: -1, price: original.price + 1 });
              expect(res.status).toBe(400);
            
              const after = await request(app).get(`/products/${original.id}`);
              expect(after.body).toEqual(original);
            });

            The PATCH response answers “did the API report the rejection?” The GET answers “did it keep the product safe?” Only the second is about the data.

            In short

            Each PATCH test proves one promise. The 404 test uses a well-formed absent id. To prove unsent fields stayed, compare the whole product and read it back. To prove a rejection wrote nothing, read the product again after the 400.

            CHECK · predict before you choose

            Test D sends a valid price together with the invalid quantity. Why?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • To make the request look like a real client’s — Realism is not the point; the extra field is there to catch a specific bug.
            • To prove the service did not save part of the change — If the price had been written before the quantity was checked, the read-back would show the new price.
            • Because PATCH needs at least two fields — PATCH accepts one field; the happy-path test sends only the price.

            Fake, mock, spy: control and observation

            In testing, a dependency is anything the function under test calls. For the service, that is the repository. A unit test replaces it with a test double: a stand-in the test controls, so the result depends only on the function being tested.

            A mock stands in for the repositoryA test calls the real service. The service calls the repository, which the test has replaced with a mock, so MongoDB behind it is never reached. Two arrows: control, the answer the mock gives the service; observe, the record of how the mock was called, which the test reads.observe: how it was calledTestit(...)callsServicethe real codecallsMockrepositorycontrol: what it returnsMongoDBnot reachedthe real repositorywould go on to here
            Notice the two powers: the test decides what the mock answers, and afterwards it asks the mock how it was called.
            WordWhat it isIn Vitest
            FakeA stand-in function that does nothing but remember every callvi.fn()
            MockA fake that answers with something the test chosevi.fn().mockReturnValue(4)
            SpyA wrapper around a real function that records its calls. It still runs the real code unless you give it an answer.vi.spyOn(inventory, 'countOnHand')
            Test doubleThe umbrella word for all three

            The Vitest documentation calls everything vi.fn() returns a mock function. The notes keep fake and mock apart by use: the same object is a fake while it only listens and a mock once it answers.

            A double gives a test two powers:

            • Control: what it returns, resolves to, or throws. The service sees exactly the products the test chose.
            • Observation: how it was called, how many times, and with what. The test asks afterwards.

            The tests in this chapter use one power, the other, or both. The low-stock test only controls what the repository returns. The never-write test only observes that the write was not called. The update test does both.

            In short

            A dependency is what the function under test calls; a test double stands in for it. A fake remembers calls, a mock also answers, and a spy wraps a real function. Doubles give control over answers and observation of calls.

            CHECK · predict before you choose

            A test wraps productsRepository.updateProduct with vi.spyOn and gives it no answer. The service then calls it. What runs?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • Nothing; a spy returns undefined until you give it an answer — That is vi.fn(). A spy keeps the real function underneath.
            • Vitest throws, because spies must not be called — A spy records calls; being called is what it is there to notice.
            • The real updateProduct, and the spy records the call — vi.spyOn alone watches and calls through, so the real repository runs.

            vi.fn: a function that remembers

            vi.fn() makes a function with no behaviour that remembers every call. From the notes:

            src/mocks-and-spies.unit.test.ts · from the Week 5 notes
            it('a fake function remembers how it was called', () => {
              const notify = vi.fn();
            
              notify('order shipped', 3);
            
              expect(notify).toHaveBeenCalledWith('order shipped', 3);
              expect(notify).toHaveBeenCalledTimes(1);
            });

            The same idea, with the result checked too:

            TypeScript · composed for this guide, run with Vitest 3.2
            const notify = vi.fn();
            const result = notify('restock', 4);
            
            expect(result).toBeUndefined();
            expect(notify).toHaveBeenCalledTimes(1);
            expect(notify).toHaveBeenCalledWith('restock', 4);

            A bare fake returns undefined. Watch one trap: expect(notify()).toBeUndefined() calls notify again, so the call count is now 2. Keep the result in a variable, as above, and assert on the variable.

            From the notes, a fake told what to answer:

            src/mocks-and-spies.unit.test.ts · from the Week 5 notes
            it('a fake function can be told what to answer', () => {
              const rollDie = vi.fn().mockReturnValue(4);
            
              expect(rollDie()).toBe(4);
              expect(rollDie()).toBe(4);
            });

            Three ways to choose the answer:

            TypeScript · composed for this guide, run with Vitest 3.2
            const readStock = vi.fn().mockReturnValue(8);
            expect(readStock()).toBe(8);
            expect(readStock('any id')).toBe(8);
            
            const next = vi.fn()
              .mockReturnValue(8)
              .mockReturnValueOnce(0)
              .mockReturnValueOnce(3);
            // next() gives 0, then 3, then 8 every time after
            
            const total = vi.fn().mockImplementation(
              (price: number, quantity: number) => price * quantity
            );
            expect(total(2.5, 4)).toBe(10);
            MethodAnswers
            mockReturnValue(v)Every call, whatever the arguments
            mockReturnValueOnce(v)The next call only. Several queue up in the order written; when the queue is empty, the default answers.
            mockImplementation(fn)Runs your function with the call’s arguments. vi.fn(fn) does the same.

            In Python, unittest.mock does the same job. One difference: calling a bare Mock() returns another Mock, not None.

            Python

            from unittest.mock import Mock
            notify = Mock()
            notify('restock', 4)
            notify.assert_called_once_with('restock', 4)
            print(notify.call_count)  # → 1
            read_stock = Mock(return_value=8)
            print(read_stock())  # → 8

            TypeScript

            const notify = vi.fn();
            const result = notify('restock', 4);
            expect(notify).toHaveBeenCalledTimes(1);
            expect(notify).toHaveBeenCalledWith('restock', 4);
            const readStock = vi.fn().mockReturnValue(8);
            expect(readStock()).toBe(8);

            In short

            vi.fn() makes a fake that returns undefined and records every call; check it with toHaveBeenCalledTimes and toHaveBeenCalledWith. Calling it inside expect counts as a call. mockReturnValue answers always, mockReturnValueOnce queues one-time answers, mockImplementation runs your function.

            CHECK · predict before you choose

            notify was called once. The test then runs expect(notify()).toBeUndefined() and expect(notify).toHaveBeenCalledTimes(1). What happens?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • Both pass — The first expect called notify again, so the count is no longer 1.
            • The count assertion fails, because the first expect made a second call — notify() inside expect is a real call; the fake records it like any other.
            • The first assertion fails, because a fake returns null — A bare vi.fn() returns undefined, so that assertion passes.

            Resolve, reject, and the order of once

            Repository functions are async: they return a Promise. A fake for one has to hand back a Promise too. From the notes:

            src/mocks-and-spies.unit.test.ts · from the Week 5 notes
            it('an async fake answers with a resolved promise', async () => {
              const loadSettings = vi.fn().mockResolvedValue({ theme: 'dark' });
            
              expect(await loadSettings()).toEqual({ theme: 'dark' });
            });

            Success and failure:

            TypeScript · composed for this guide, run with Vitest 3.2
            const loadProduct = vi.fn().mockResolvedValue({ price: 20 });
            expect(await loadProduct()).toEqual({ price: 20 });
            
            const failing = vi.fn().mockRejectedValue(new Error('Database unavailable'));
            await expect(failing()).rejects.toThrow('Database unavailable');
            ToSync functionAsync function
            AnswermockReturnValue(v)mockResolvedValue(v)
            FailmockImplementation(() => { throw err; })mockRejectedValue(err)
            Answer oncemockReturnValueOnce(v)mockResolvedValueOnce(v), mockRejectedValueOnce(err)
            Check a failureexpect(() => fn()).toThrow()await expect(fn()).rejects.toThrow()

            The sync check takes a function, so expect can call it and catch the throw. The async check takes the Promise and must be awaited. Vitest 3 warns about a forgotten await and checks it late; do not rely on that.

            Once answers go first, whatever order you write them in:

            TypeScript · composed for this guide, run with Vitest 3.2
            const connect = vi.fn()
              .mockResolvedValue('ready')
              .mockRejectedValueOnce(new Error('Temporary failure'));
            
            await expect(connect()).rejects.toThrow('Temporary failure');
            expect(await connect()).toBe('ready');

            The first call rejects; every call after it resolves to ready. A sequence only decides what each call gets. It does not make your code retry; the code under test has to do that.

            Python

            import asyncio
            from unittest.mock import AsyncMock
            load_product = AsyncMock(return_value={'price': 20})
            print(asyncio.run(load_product()))  # → {'price': 20}
            failing = AsyncMock(side_effect=Exception('Database unavailable'))
            try: asyncio.run(failing())
            except Exception as e: print(e)  # → Database unavailable

            TypeScript

            const loadProduct = vi.fn().mockResolvedValue({ price: 20 });
            expect(await loadProduct()).toEqual({ price: 20 });
            const failing = vi.fn().mockRejectedValue(new Error('Database unavailable'));
            await expect(failing()).rejects.toThrow('Database unavailable');

            In short

            An async fake resolves with mockResolvedValue or rejects with mockRejectedValue. Check a rejection with await expect(promise).rejects.toThrow(); a sync throw with expect(() => fn()).toThrow(). Once answers are used first, then the default.

            CHECK · predict before you choose

            connect is vi.fn().mockResolvedValue('ready').mockRejectedValueOnce(new Error('x')). It is called three times. What does each call give?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • Rejects, then ready, then ready — The one-time rejection is used first; after that the default answers every call.
            • Ready, then rejects, then ready — The order you write them in does not matter; once answers always go first.
            • Rejects all three times — Once means one call; the queue empties after the first.

            Read the call history

            Every fake and spy keeps a record of its calls. The matchers read it for you:

            TypeScript · composed for this guide, run with Vitest 3.2
            const notify = vi.fn();
            notify('low-stock', 2);
            notify('restocked', 12);
            
            expect(notify).toHaveBeenCalledTimes(2);
            expect(notify).toHaveBeenCalledWith('low-stock', 2);
            expect(notify).toHaveBeenNthCalledWith(2, 'restocked', 12);
            expect(notify).toHaveBeenLastCalledWith('restocked', 12);
            expect(notify.mock.calls).toEqual([['low-stock', 2], ['restocked', 12]]);
            expect(notify.mock.calls[0][1]).toBe(2);
            MatcherPasses when
            toHaveBeenCalled()At least one call
            toHaveBeenCalledTimes(n)Exactly n calls
            toHaveBeenCalledWith(...)At least one call had these arguments
            toHaveBeenNthCalledWith(n, ...)Call number n, counting from 1, had these arguments
            toHaveBeenLastCalledWith(...)The latest call had these arguments

            toHaveBeenCalledWith alone does not mean once: ten calls with one match still pass. Pair it with toHaveBeenCalledTimes(1) when the count matters.

            mock.calls is the raw record: one array of arguments per call. mock.results records what each call gave back: whether it returned or threw, and the value:

            TypeScript · composed for this guide, run with Vitest 3.2
            const addTax = vi.fn((price: number) => price * 1.2);
            addTax(10);
            expect(addTax.mock.results[0]).toEqual({ type: 'return', value: 12 });

            A call that threw has type: 'throw'. For an async fake, the value is the Promise itself; mock.settledResults holds what it settled to.

            One trap: the record keeps a reference to each argument, not a copy. Change the object after the call and the record changes too:

            TypeScript · composed for this guide, run with Vitest 3.2
            const save = vi.fn();
            const changes = { price: 12.5 };
            save(changes);
            changes.price = 20;
            expect(save.mock.calls[0][0].price).toBe(20); // not 12.5

            If the code under test changes an object after passing it on, take a snapshot inside the fake with structuredClone and assert on the copy:

            TypeScript · composed for this guide, run with Vitest 3.2
            const captured: object[] = [];
            const save = vi.fn((changes: object) => {
              captured.push(structuredClone(changes));
            });

            In short

            toHaveBeenCalledWith means at least one matching call; add toHaveBeenCalledTimes for an exact count. toHaveBeenNthCalledWith counts from 1. mock.calls holds arguments, mock.results holds what came back. The record keeps references, so snapshot objects that change later.

            CHECK · predict before you choose

            notify was called twice, both times with ('low-stock', 2). Which assertion fails?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • expect(notify).toHaveBeenCalledWith('low-stock', 2) — At least one call matched, so this passes.
            • expect(notify).toHaveBeenLastCalledWith('low-stock', 2) — The latest call had exactly these arguments, so this passes.
            • expect(notify).toHaveBeenCalledTimes(1) — There were two calls. This is the only assertion that notices.

            vi.spyOn wraps a real method

            vi.fn() makes a new function. vi.spyOn(object, name) wraps one that already exists, on an object, under its own name. On its own, the spy only watches; the real function still runs. From the notes:

            src/mocks-and-spies.unit.test.ts · from the Week 5 notes
            const inventory = {
              countOnHand: () => 42
            };
            
            it('vi.spyOn alone watches, but does not replace', () => {
              const count = vi.spyOn(inventory, 'countOnHand');
            
              expect(inventory.countOnHand()).toBe(42);
              expect(count).toHaveBeenCalledTimes(1);
            });

            Give the spy an answer and the real function stops running. The code that calls it cannot tell:

            src/mocks-and-spies.unit.test.ts · from the Week 5 notes
            it('vi.spyOn plus an answer replaces the real function', () => {
              const count = vi.spyOn(inventory, 'countOnHand').mockReturnValue(0);
            
              expect(inventory.countOnHand()).toBe(0);
              expect(count).toHaveBeenCalledTimes(1);
            });

            The same move on the repository. The service is imported for real, the repository as a namespace, import * as productsRepository, which gives vi.spyOn an object to reach into:

            TypeScript · composed for this guide, run with Vitest 3.2
            vi.spyOn(productsRepository, 'findAllProducts').mockResolvedValue([]);
            const low = await productsService.getLowStockProducts(5);
            expect(low).toEqual([]);
            RoleFunctionIn the test
            Under testproductsService.getLowStockProductsReal. Called directly.
            DependencyproductsRepository.findAllProductsSpied on and answered, so MongoDB is never reached.

            Mock the dependency, never the function under test. Spy on getLowStockProducts and give it an answer, and the test checks only that the mock returns what you told it.

            Python’s nearest tool is patch.object. It replaces by default; pass wraps= to call through like a bare spy. Leaving the with block puts the original back, which vi.restoreAllMocks() does between Vitest tests.

            Python

            from unittest.mock import patch
            class Repo:
                def find_all_products(self): return ['real']
            repo = Repo()
            with patch.object(repo, 'find_all_products', return_value=[]):
                print(repo.find_all_products())  # → []
            print(repo.find_all_products())  # → ['real']

            TypeScript

            vi.spyOn(productsRepository, 'findAllProducts').mockResolvedValue([]);
            const low = await productsService.getLowStockProducts(5);
            expect(low).toEqual([]);

            In short

            vi.spyOn(object, name) wraps an existing function and still calls it unless given an answer such as mockResolvedValue. Spy on the dependency, here the repository, and call the function under test for real.

            CHECK · predict before you choose

            A test spies on productsService.getLowStockProducts, answers with [product], and asserts the result is [product]. What does it prove?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • Nothing about the service; the real function never ran — The test checks the mock against the answer it was given. The filter was never executed.
            • That the low-stock filter works — The filter lives inside the function that was replaced, so it did not run.
            • That the repository returned [product] — The repository was never called; the mocked service answered directly.

            The service unit test file

            The notes write src/services/products-service.unit.test.ts from scratch. It starts with the imports and one hook:

            services/products-service.unit.test.ts · from the Week 5 notes
            import { beforeEach, describe, expect, it, vi } from 'vitest';
            import * as productsService from './products-service.js';
            import * as productsRepository from '../db/products-repository.js';
            import { ValidationError } from '../errors.js';
            import type { Product, ProductFields } from '../models/product.js';
            
            beforeEach(() => {
              vi.restoreAllMocks();
            });
            ImportWhy
            productsServiceThe functions under test. The file name says so: it sits next to products-service.ts.
            productsRepositoryThe functions the tests replace with spies.
            ValidationErrorTo check that a rejection is the right kind of error.
            Product, ProductFieldsTypes only, for the helper below.

            vi.restoreAllMocks() before each test puts every spied function back, so no test inherits another test’s fake answers.

            Then a fixture helper that builds a full Product from only the fields a test cares about:

            services/products-service.unit.test.ts · from the Week 5 notes
            const makeProduct = (fields: ProductFields): Product => {
              return {
                id: fields.id ?? 'aaaaaaaaaaaaaaaaaaaaaaaa',
                modelName: fields.modelName ?? 'Test Product',
                modelNumber: fields.modelNumber ?? 'TP-1',
                manufacturer: fields.manufacturer ?? 'Test',
                color: fields.color ?? 'Black',
                price: fields.price ?? 10,
                quantity: fields.quantity ?? 10
              };
            };

            These defaults are test data, separate from the defaults the real service fills in when it creates a product.

            The function under test:

            services/products-service.ts · from the Week 5 notes
            export const getLowStockProducts = async (
              threshold: number = LOW_STOCK_THRESHOLD
            ): Promise<Product[]> => {
              const products = await productsRepository.findAllProducts();
              return products.filter((product) => product.quantity < threshold);
            };

            The first real unit test feeds it three products and checks which come back:

            services/products-service.unit.test.ts · from the Week 5 notes
            it('returns only the products below the threshold', async () => {
              vi.spyOn(productsRepository, 'findAllProducts').mockResolvedValue([
                makeProduct({ modelName: 'Plenty', quantity: 100 }),
                makeProduct({ modelName: 'Few', quantity: 2 }),
                makeProduct({ modelName: 'None', quantity: 0 })
              ]);
            
              const low = await productsService.getLowStockProducts(5);
            
              const names = low.map((product) => product.modelName);
            
              expect(names).toEqual(['Few', 'None']);
            });

            Arrange: the spy answers with three products, and MongoDB is never reached. Act: call the real service with threshold 5. Assert: check the names, not just the count. The end-to-end low-stock test checks the same rule, but a failure there could be in the controller, the service, the repository or the seed data. Here it can only be the filter.

            One more question the seed data cannot answer: is a product with exactly 5 units low stock?

            services/products-service.unit.test.ts · from the Week 5 notes
            it('does not count a product sitting on the threshold', async () => {
              vi.spyOn(productsRepository, 'findAllProducts').mockResolvedValue([
                makeProduct({ modelName: 'Exactly five', quantity: 5 })
              ]);
            
              const low = await productsService.getLowStockProducts(5);
            
              expect(low).toHaveLength(0);
            });

            This pins the boundary. With < the product is not low; a change to <= fails this test and no other.

            In short

            Import the service to test it and the repository to spy on it. vi.restoreAllMocks() in beforeEach starts every test clean. makeProduct fills in test defaults. The low-stock test controls what the repository returns; the boundary test checks quantity exactly at the threshold.

            CHECK · predict before you choose

            Someone changes the filter to product.quantity <= threshold. Which of the two low-stock tests fails?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • The three-product test — Its quantities are 100, 2 and 0. None sits on 5, so < and <= give the same answer.
            • Both — Only one test has a product exactly at the threshold.
            • Only the boundary test — A product with exactly 5 now counts as low, so the expected empty list gets one item.

            Prove it never wrote

            The update service ends with one write. Two questions about it: when the change is valid, what does it send? When it is not, does it send anything at all? First, from the notes:

            services/products-service.unit.test.ts · from the Week 5 notes
            it('asks the repository to change only what it was given', async () => {
              vi.spyOn(productsRepository, 'findProduct').mockResolvedValue(
                makeProduct({ price: 10 })
              );
              const update = vi
                .spyOn(productsRepository, 'updateProduct')
                .mockResolvedValue(makeProduct({ price: 12.5 }));
            
              await productsService.updateProduct('abc', { price: 12.5 });
            
              expect(update).toHaveBeenCalledWith('abc', { price: 12.5 });
            });

            Rebuild it · put the lines in order

            Ask the spy what it was sent

            Rebuild the notes’ test that checks the service passes on only the fields it was given. Arrange both spies, act once, then ask the write spy.

            Show the solved code
            it('asks the repository to change only what it was given', async () => {
              vi.spyOn(productsRepository, 'findProduct').mockResolvedValue(
                makeProduct({ price: 10 })
              );
              const update = vi
                .spyOn(productsRepository, 'updateProduct')
                .mockResolvedValue(makeProduct({ price: 12.5 }));
              await productsService.updateProduct('abc', { price: 12.5 });
              expect(update).toHaveBeenCalledWith('abc', { price: 12.5 });
            });

            Both spies have to be in place before the service runs, and the question to the write spy only makes sense after the call. The two spies do not depend on each other, so either may come first.

            Both repository functions are spied on and answered, for different reasons. findProduct is there only for its answer: the service needs a stored product to compare against. updateProduct is the one the test asks about, so the spy is kept in a variable. The id 'abc' is fine here because the repository is replaced; an end-to-end test would send it to new ObjectId and get a 500.

            Now the rejected change:

            services/products-service.unit.test.ts · from the Week 5 notes
            it('writes nothing when quantity would go negative', async () => {
              vi.spyOn(productsRepository, 'findProduct').mockResolvedValue(
                makeProduct({ quantity: 10 })
              );
              const update = vi.spyOn(productsRepository, 'updateProduct');
            
              await expect(
                productsService.updateProduct('abc', { quantity: -1 })
              ).rejects.toThrow(ValidationError);
            
              expect(update).not.toHaveBeenCalled();
            });

            Rebuild it · put the lines in order

            Prove the write never happened

            Rebuild the notes’ test for a change that would make quantity negative. It must reject, and the write must never be called.

            Show the solved code
            it('writes nothing when quantity would go negative', async () => {
              vi.spyOn(productsRepository, 'findProduct').mockResolvedValue(
                makeProduct({ quantity: 10 })
              );
              const update = vi.spyOn(productsRepository, 'updateProduct');
              await expect(
                productsService.updateProduct('abc', { quantity: -1 })
              ).rejects.toThrow(ValidationError);
              expect(update).not.toHaveBeenCalled();
            });

            The spies go in before the call, the call must be awaited until it rejects, and only then does not.toHaveBeenCalled prove the write never happened. The two spies may come in either order.

            The write spy gets no answer on purpose: it should never be called. If a bug skipped the check, the call would go through to the real repository, which has no database here, and the test would go red either way. Compare test D, which needed a second request to see the damage; here not.toHaveBeenCalled() asks the one function that could do it.

            Now You Try #2 asks for the price version and the missing-product case. Composed here, and run against a copy of the notes’ service:

            TypeScript · composed for this guide, run with Vitest 3.2
            it('writes nothing when the price would go negative', async () => {
              vi.spyOn(productsRepository, 'findProduct').mockResolvedValue(
                makeProduct({ price: 10 })
              );
              const update = vi.spyOn(productsRepository, 'updateProduct');
            
              await expect(
                productsService.updateProduct('abc', { price: -1 })
              ).rejects.toThrow(ValidationError);
            
              expect(update).not.toHaveBeenCalled();
            });
            
            it('returns null when there is no product with that id', async () => {
              vi.spyOn(productsRepository, 'findProduct').mockResolvedValue(null);
              const update = vi.spyOn(productsRepository, 'updateProduct');
            
              const result = await productsService.updateProduct('abc', { price: 12.5 });
            
              expect(result).toBeNull();
              expect(update).not.toHaveBeenCalled();
            });

            The missing-product test has no .rejects: the service does not throw, it returns null. It asserts two things, the result and the absent write.

            EvidenceAsksMatcher
            The resultWhat did the function hand back or throw?rejects.toThrow(ValidationError), toBeNull()
            The side effectDid it try to change anything?expect(update).not.toHaveBeenCalled()

            In short

            A spy kept in a variable answers both questions about a write: toHaveBeenCalledWith shows what was sent, not.toHaveBeenCalled shows nothing was. The write spy needs no answer when it must never be called. Check the result and the side effect separately.

            CHECK · predict before you choose

            Why does the write spy in the negative-quantity test get no mockResolvedValue?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • The test expects it never to be called, so it needs no answer — An answer would only matter if the service reached it, which is the bug the test looks for.
            • A spy cannot be given an answer — The update test gives the same spy an answer with mockResolvedValue.
            • So the real database records the attempt — There is no database in a unit test; the spy itself records any call.

            Creation fills the blanks

            The last Now You Try #2 task: call createProduct with only a model name and prove that what reached the repository had all six fields. The notes do not print the service’s defaults; this version assumes the Week 4 ones, an empty string for text and 0 for numbers.

            TypeScript · composed for this guide, run with Vitest 3.2
            describe('createProduct', () => {
              it('fills in defaults before handing it to the repository', async () => {
                const created = makeProduct({});
                const insert = vi
                  .spyOn(productsRepository, 'insertProduct')
                  .mockResolvedValue(created);
            
                const result = await productsService.createProduct({ modelName: 'Desk Lamp' });
            
                expect(insert).toHaveBeenCalledWith({
                  modelName: 'Desk Lamp',
                  modelNumber: '',
                  manufacturer: '',
                  color: '',
                  price: 0,
                  quantity: 0
                });
                expect(insert).toHaveBeenCalledTimes(1);
                expect(result).toEqual(created);
              });
            });

            insertProduct is watched and answered: the service returns whatever comes back from it, so the test gives it makeProduct({}) and checks the service passes that on. toHaveBeenCalledWith compares the whole object, so a missing field or an extra one fails. The id is not in it; the repository makes the id.

            QuestionUnit testEnd-to-end: POST, then GET
            Did the service build all six fields?Yes, directlyOnly through what came back
            Were they stored and returned?No, the repository is a spyYes
            If it fails, where is the bug?In createProductAnywhere from the route to MongoDB

            The end-to-end version is the test that caught the Week 4 defaults bug, where the defaults reached the response but never the database. The unit test alone would have passed there, because the bug was in what got stored. Each test isolates a different part.

            In short

            The createProduct unit test answers insertProduct with makeProduct({}) and checks it received all six fields, defaults included, exactly once. It proves what the service built; the end-to-end POST then GET proves what was stored.

            CHECK · predict before you choose

            The createProduct unit test passes, but the end-to-end POST then GET test finds modelNumber missing from the stored product. Where do you look first?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • createProduct in the service — The unit test just showed the service sends all six fields.
            • The repository’s insert path — The service handed over the right data, so the loss happens at or after the repository.
            • makeProduct in the unit test — makeProduct only builds the fake answer; it cannot change what the end-to-end test stores.

            Clear, reset, restore

            A fake carries two things from test to test, its call history and its answers; a spy also hides the original function. Three methods wipe more or less of that:

            What each cleanup wipesThree rows, one per method. mockClear clears the call history and keeps the answers and the original. mockReset clears the history and the answers. mockRestore clears both and, for a spy, puts the original method back.callsanswersoriginalmockClear()clearedkeptkeptmockReset()clearedclearedkeptmockRestore()clearedclearedput back
            Notice that each row does everything the row above it does, and one thing more.
            MethodCall historyAnswers you configuredOriginal method
            mockClear()ClearedKeptStill replaced
            mockReset()ClearedCleared, back to how the fake startedStill replaced
            mockRestore()ClearedClearedPut back, for a spy
            TypeScript · composed for this guide, run with Vitest 3.2
            const count = vi.fn(() => 4);
            count.mockReturnValue(9);
            expect(count()).toBe(9);
            
            count.mockClear();
            expect(count).not.toHaveBeenCalled();
            expect(count()).toBe(9);
            
            count.mockReset();
            expect(count()).toBe(4);

            mockReset goes back to the function given to vi.fn, so 4, not undefined. On a plain vi.fn() there is no original to put back, so mockRestore acts like reset.

            The course runs vi.restoreAllMocks() in beforeEach. Its job is putting back every function vi.spyOn replaced, which is what the service tests need. What it does to a standalone vi.fn() has changed between Vitest versions, so do not rely on it there. It never empties a database, and a module replaced with vi.mock stays replaced.

            The Vitest guide also offers the same cleanup as configuration, so no file can forget it: test: { restoreMocks: true } in vitest.config.ts.

            In short

            mockClear wipes the call history and keeps answers. mockReset also removes configured answers, back to how the fake started. mockRestore also puts a spied original back. vi.restoreAllMocks restores spies; it does not reset the database or undo vi.mock.

            CHECK · predict before you choose

            count has mockReturnValue(9) and some recorded calls. Which call forgets the calls but keeps answering 9?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • count.mockReset() — Reset also removes the configured answer, so 9 is gone.
            • count.mockRestore() — Restore does everything reset does, and more.
            • count.mockClear() — Clear wipes only the call history; the configured answer stays.

            Mock a whole module

            vi.spyOn needs an object to reach into. When code imports a single function by name, there is none, so Vitest can replace the whole module instead. An example in the style of the Vitest guide:

            stock.ts · composed for this guide
            export async function readQuantity(id: string): Promise<number> {
              return 10;
            }
            stock-label.ts · composed for this guide
            import { readQuantity } from './stock.js';
            
            export async function stockLabel(id: string): Promise<string> {
              const quantity = await readQuantity(id);
              return quantity === 0 ? 'Unavailable' : 'Available';
            }
            stock-label.unit.test.ts · composed for this guide, run with Vitest 3.2
            import { expect, it, vi } from 'vitest';
            import { readQuantity } from './stock.js';
            import { stockLabel } from './stock-label.js';
            
            vi.mock(import('./stock.js'), () => ({ readQuantity: vi.fn() }));
            
            it('labels a product with no stock', async () => {
              vi.mocked(readQuantity).mockResolvedValue(0);
            
              expect(await stockLabel('lamp-1')).toBe('Unavailable');
              expect(readQuantity).toHaveBeenCalledWith('lamp-1');
            });
            PieceIts job
            vi.mock(import(...), factory)Replaces the module’s exports with what the factory returns, for every import of it in this test file. The import() form lets TypeScript check the path.
            vi.fn()Makes the replacement function inside the factory.
            vi.mocked(readQuantity)Changes nothing at run time. It tells TypeScript the import is already a mock, so mockResolvedValue type-checks.

            vi.mock is hoisted: Vitest moves it above the imports, so the module is replaced before anything loads it. The factory therefore cannot use variables declared further down the file.

            For this assignment, repository spies are enough. The service imports the repository as a namespace, so vi.spyOn can reach every function in it.

            In short

            vi.mock(import(path), factory) replaces a module’s exports for the test file and is hoisted above the imports. vi.fn builds the replacements; vi.mocked only adds types. Use it when there is no object to spy on; this assignment does not need it.

            CHECK · predict before you choose

            What does vi.mocked(readQuantity) do at run time?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • Nothing; it only tells TypeScript the import is already a mock — vi.mock did the replacing. vi.mocked is a typing helper.
            • It replaces readQuantity with a mock — The replacing happened in vi.mock’s factory, before any import ran.
            • It restores the real readQuantity — Restoring is mockRestore’s job, and a vi.mock module stays replaced.

            When a test lies

            A test can pass without testing anything, or fail for a reason that has nothing to do with the code. Common cases:

            SymptomLikely issueWhat to check
            Passes, but the logic never ranThe function under test was mockedSpy only on what it calls
            A unit test reaches MongoDBA repository function it calls is still realEvery function it reaches has an answer, or is meant to fail
            A rejection is never checkedThe expect(...).rejects was not awaitedAwait every promise assertion
            Passes alone, fails with the othersLeftover spies or data from another testvi.restoreAllMocks() and the database reset in beforeEach
            An update test passes with no updateThe new value equals the seedA value no seed product has
            400, but the data changedOnly the response was checkedRead the product back, or spy on the write
            A threshold bug slips throughNo value sits on the boundaryOne product exactly at the threshold
            A call-history assertion shows a later valueThe argument was changed after the callstructuredClone a copy at call time
            An absent-id test gets 500, not 404The id is malformed, not absent'a'.repeat(24)
            Every test runs twiceCompiled copies of the tests are found tooWhere Vitest looks for test files
            The unit command is green with nothing run--passWithNoTestsThe test count in the output

            Seven questions to judge any test:

            1. What behaviour does it check?
            2. Which realistic bug would make it fail?
            3. Does it include the boundary value?
            4. Does it check which items, not only how many?
            5. Does it check side effects, such as a write?
            6. Could another test change its starting state?
            7. What is outside its evidence?

            A passing suite is evidence for the scenarios it runs. It is not proof that there are no bugs.

            In short

            A test can lie by mocking the function under test, skipping await, inheriting state, reusing a seed value, checking only the response, or missing the boundary. Ask which realistic bug would make each test fail.

            CHECK · predict before you choose

            A unit test passes alone but fails when the whole file runs, and the file has no beforeEach. The most likely cause?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • A spy from an earlier test is still answering — Nothing restored it, so the next test inherits a fake it did not set up.
            • Vitest runs each test twice when the whole file runs — Vitest runs each test once; a duplicate run comes from duplicate files.
            • The low-stock threshold is wrong — A wrong threshold fails alone too; this fails only in company.

            Say it, then try it

            Answer each question aloud first, then open it.

            1. A service calls res.status(400). What went wrong?

            It crossed into HTTP. The service throws a ValidationError; the controller or the error handler chooses the status.

            2. Why does the repository, not the service, turn a document into a Product?

            So MongoDB details such as ObjectId and _id stay in one layer, and the service handles only Products.

            3. A PATCH sets quantity to 0. With ??, which quantity does the service check?

            0. ?? falls back only on null or undefined, and 0 is neither.

            4. Why is a 400 response alone not enough to prove a rejection was safe?

            The service could write first and report the error after. Read the product back, or spy on the write.

            5. A low-stock test uses quantities 1 and 8 with threshold 5. What can it miss?

            The difference between < and <=. Add a product at exactly 5.

            6. What does a bare vi.fn() return?

            undefined.

            7. Does vi.spyOn alone stop a write from reaching the database?

            No. Without an answer it calls the real function.

            8. mockReturnValue or mockResolvedValue: what is the difference?

            The first hands back the value. The second hands back a Promise that resolves to it, for an async function.

            9. Does toHaveBeenCalledWith prove one call?

            No, only that at least one call matched. Add toHaveBeenCalledTimes(1).

            10. An object is changed after it was passed to a fake. What does mock.calls show?

            The changed object. The record keeps a reference, not a copy.

            11. Which cleanup forgets the calls but keeps a configured answer?

            mockClear.

            12. Does vi.mocked replace a function?

            No. It only tells TypeScript the value is already a mock.

            13. Why can a unit test use the id 'abc' when an end-to-end test cannot?

            In the unit test the repository is replaced, so new ObjectId never runs. End to end, it throws and the answer is 500.

            14. What should the missing-product unit test assert?

            That the result is null and that the write was never called.

            15. Is regression testing only for unit tests?

            No. It means rerunning every test that passed before, unit and end-to-end alike.

            Then five tasks. Write each test before you open its answer.

            A · The zero boundary. Update quantity to 0 for a stored product. What should the test assert?

            The promise resolves, the write spy received ('abc', { quantity: 0 }), and it was called once. 0 is allowed; only values below 0 are rejected.

            B · An accidental full update. The request sends only the price. How do you catch a service that passes every field?

            Assert the exact argument, toHaveBeenCalledWith('abc', { price: 12.5 }). An extra field fails it; expect.objectContaining would let it through.

            C · The database fails. findProduct rejects. What should the test check?

            Answer findProduct with mockRejectedValue(new Error('Database unavailable')), await rejects.toThrow with that message, and check the write spy was never called.

            D · Predict. next is vi.fn().mockReturnValue('normal').mockReturnValueOnce('first'). The test calls next(), then next.mockClear(), then next() again. What are the two results and the final call count?

            'first', then 'normal', and a count of 1. mockClear wiped the first call from the history; the once-answer was already used.

            E · Every unit test passes, but the end-to-end PATCH returns the wrong shape. Where could the fault be?

            Outside what the unit tests replaced: the repository’s conversion to a Product, the controller’s response, the route or middleware setup, or the seed data.

            Run both suites when you are done: npm run test:unit, then npm test, and read the counts as well as the colours.

            Week 6 continues here: Know who is asking. Decide what they may do.

            In short

            Explain the layers, the three PATCH outcomes, and the difference between a result and a side effect. Then write the zero boundary, the exact-argument check, the database failure, and read both suites’ counts.

            CHECK · predict before you choose

            Which single assertion shows that a rejected update did not write?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • expect(res.status).toBe(400) — The status reports the rejection; a buggy service could still have written first.
            • expect(update).toHaveBeenCalledWith('abc', { price: -1 }) — That asserts a write happened, the opposite of what you want.
            • expect(update).not.toHaveBeenCalled() — It asks the only function that could write whether it was ever called.

            Source trail · Week 5 notes (Testing), captured October 6, 2026, and the Vitest guide to mock functions. The Python examples and the composed tests were run.

            HW2

            HW2 · SliceDrop with MongoDB: what each task asks, where the course taught it, how to check yourself

            Homework 2: plan it before you build it.

            Three things change from HW1

            Homework 2 takes the SliceDrop API from Homework 1 (four endpoints, two roles) and changes three things. The public HW2 instructions ↗ are the contract. This chapter maps each part of them to the lesson that taught it and shows each pattern with class exercise code. It shows no homework code, because classmates are doing this homework while this page is public.

            In HW1In HW2Where the course taught it
            Orders live in an array in memory and are gone after a restart.Orders are documents in MongoDB. The menu is seeded for you; placing an order writes a new document.Week 3: move state into a database
            Routers hold most of the logic.One folder per layer: routes, controllers, services, db. Each kind of logic has one home.Week 4: one job per layer and Week 5: four layers, four jobs
            Validation is synchronous: the menu is an array you can search at once.Finding a menu item now returns a Promise, so the order validator becomes async, and so does everything that calls it.async and await and Week 5: three outcomes

            The restaurant grows too. The menu gains larger sizes, crusts with prices, premium toppings and desserts. Staff get full menu CRUD and can move orders through statuses, and each order is priced when it is placed.

            A fresh clone does not compile, on purpose. The instructions say npm run typecheck fails in exactly two places. The menu types cannot yet describe the new menu, and the HW1 order validator breaks because finding a menu item now returns a Promise. Make the types describe the data (the data is correct, so do not edit it), make the validator async, and the compiler goes quiet. From then on, the tests are your to-do list.

            The Promise rule to remember: a function that awaits must be async, and whoever calls it must await it too. One Promise at the bottom climbs all the way up. A caller that forgets to await holds a Promise, not the answer, and a Promise is always truthy.

            In short

            HW2 keeps the HW1 contract and changes three things: where the data lives (MongoDB), where each kind of logic lives (one folder per layer) and when an answer is ready (validation is now async). The two typecheck errors on a fresh clone are the first task.

            CHECK · predict before you choose

            On a fresh clone, npm run typecheck fails in the HW1 order validator. Which cause do the instructions name?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • The validation rules changed for HW2 — The instructions say the rules do not change. Only the timing does.
            • Finding a menu item now returns a Promise, so the validator must become async — A lookup in the database is asynchronous, and the validator calls it, so the validator has to await and be async.
            • MongoDB is not running — Typecheck never connects to anything. It only reads the types in the code.

            Every task has a layer and a lesson

            The TODO comments in the repository carry the details; the instructions carry the contract. This table puts each task in the layer where it belongs, names the lesson that teaches its pattern, and lists the status codes the instructions give it.

            TaskLayerWhere the course taught itStatus codes
            Describe the new menu: a dessert category and crusts that have pricesmodels (types)unions and types describe your assumptionsNone. npm run typecheck is the test.
            Make order validation async; the rules stay the samevalidationHW1 validation and await400 for an invalid order
            Validate menu bodies: every required field for a create, only the fields present for a changevalidationcollect every error and a PATCH changes only what is sent400 for an invalid body
            Wire each route: token middleware, the id check, then the controllerroutesrouters, middleware order and guards before the controller401 for a missing or wrong token; 400 for a malformed order id
            Create, change and delete menu itemscontrollers, then dbcreation and deletion and 409 for a name that is taken201, 200, 204 with no body; 400, 404, 409
            Place an order, list orders (with a status filter), read one, change its statuscontrollers, services, dbGET by id and shape and existence201 with the total, 200; 400, 404
            Price an orderservicesone job per layerNone of its own. The total travels in the 201.
            Store and read menu items and ordersdb (repositories)CRUD and Mongo behind HTTP routesNone. It returns a value, null or a boolean, and the controller chooses the code.

            Two things in that table are easy to miss. The menu has no service layer: its controllers call the data layer directly, because the menu has no business rules and a layer that only passes calls along adds nothing. And the endpoint table lists no 403. With the shared secrets from HW1, a missing or wrong token is a 401; 403 arrives with roles in Week 6.

            Every error response is still JSON with an error or errors field, never HTML, as in HW1.

            In short

            Each HW2 task sits in one layer: types in models, rules in validation, wiring in routes, status codes in controllers, pricing in services and Mongo calls in db. Find the layer first, then open the lesson that taught it.

            CHECK · predict before you choose

            A staff member creates a menu item with an id that already exists. Where does the 409 decision belong?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • In the menu-body validator, with the other 400 checks — The instructions say validation checks shape only. Whether an id is taken needs a lookup, and its answer is a 409, not a 400.
            • In the router, before the controller — A router only wires middleware and controllers together. It does not look anything up.
            • In the controller, after validation passes — The instructions make it the controller’s question: the body has the right shape, and only a lookup can tell whether the id is free.

            Each pattern, in class code

            Every pattern HW2 needs appears in a class exercise first. Each excerpt below is copied from a class repository and names its file and lines. Read the lesson it links to, then write your own version for SliceDrop.

            A repository: Mongo in, plain values out

            The Week 6 products repository is the example to follow for a data layer. Each function makes one Mongo call and returns a plain value: a converted document, null or a boolean. It never chooses a status code.

            TypeScript · Week 6 · src/db/products-repository.ts:12–22
            const toProduct = (doc: Document): Product => {
              return {
                id: doc._id.toString(),
                modelName: doc.modelName,
                modelNumber: doc.modelNumber,
                manufacturer: doc.manufacturer,
                color: doc.color,
                price: doc.price,
                quantity: doc.quantity
              };
            };

            toProduct is the only function that knows about _id. It turns the ObjectId into a string id, so no layer above ever handles a Mongo type (ids).

            TypeScript · Week 6 · src/db/products-repository.ts:72–77
            export const deleteProduct = async (id: string): Promise<boolean> => {
              const result = await getDb()
                .collection(PRODUCTS)
                .deleteOne({ _id: new ObjectId(id) });
              return result.deletedCount === 1;
            };

            A delete reports whether anything was deleted, and the controller decides what that means in HTTP. An insert works the same way: insertOne hands back insertedId, which becomes the new string id (lines 36–56 of the same file). An update uses findOneAndUpdate with $set and returnDocument: 'after', and gives back null when nothing matched (lines 58–70; follow one PATCH request).

            A 409 is a named error

            TypeScript · Week 6 · src/services/auth-service.ts:25–29
              // 1. is the username free?
              const existing = await usersRepository.findUserByUsername(username);
              if (existing) {
                throw new ConflictError('That username is taken');
              }
            TypeScript · Week 6 · src/middleware/error-handler.ts:27–30
              if (err instanceof ConflictError) {
                res.status(409).json({ error: err.message });
                return;
              }

            Registering checks whether the username exists and throws a ConflictError if it does; the error handler turns that class into a 409 (registering a user). The check is a question about what is stored, not about the shape of the request, so it comes after validation and before the write. Week 6 asks it in the service; HW2’s instructions give the same kind of question to the menu controller.

            A delete answers 204 with no body

            TypeScript · Week 4 · src/controllers/product-controllers.ts:23–30
            const deleteProduct = async (req: Request, res: Response): Promise<void> => {
              const deleted = await productService.remove(String(req.params.id));
              if (!deleted) {
                res.status(404).json({ error: 'No product with that id' });
                return;
              }
              res.status(204).send();
            };

            When nothing was deleted the controller answers 404. Otherwise it answers 204 and calls send() with nothing in it, because a 204 carries no body (follow creation and deletion).

            Guards run before the controller, in order

            TypeScript · Week 6 · src/routes/products-router.ts:16–21
            productsRouter.patch(
              '/:id',
              requireJwt,
              requireRole('staff'),
              productsController.patchProduct
            );
            TypeScript · Week 4 · src/routes/product-routes.ts:9
            productRouter.get('/:id', validateId, productControllers.getProduct);

            A route lists its middleware from left to right, and each one either answers or calls next (middleware either continues or answers). Week 6 checks who you are, then what you may do, then runs the controller. Week 4 checks the shape of the id before the controller looks it up. HW2’s instructions put the two together: the token middleware first, then the id check, then the controller. Who are you comes before is your request well formed.

            A filter reads req.query

            TypeScript · Week 6 · src/controllers/products-controller.ts:8–12
              if (req.query.lowStock === 'true') {
                res.json(await productsService.getLowStockProducts());
                return;
              }
              res.json(await productsService.getAllProducts());

            Query values arrive as strings, so the class code compares with the string 'true'. The filter is optional: with no query, the controller returns everything. HW2’s GET /orders?status= is the same idea with a different field. Decide which layer does the filtering, and be ready to say why.

            Async errors reach the error handler

            TypeScript · Week 6 · src/services/products-service.ts:55–57
              if (quantity < 0) {
                throw new ValidationError('quantity cannot be negative');
              }
            TypeScript · Week 6 · src/app.ts:13
            app.use(errorHandler);

            The service throws inside an async function, and the controller that awaits it does not catch. Express 5 sees the rejected Promise and passes the error to the error handler, which is registered last, after the routers (put errors last). In Express 4 you had to catch the error and call next(err) yourself. The one thing your code must still do is await: an error inside a Promise that nobody awaits never reaches the handler (three outcomes).

            In short

            The class code already shows every HW2 pattern: a repository that hides _id and returns null or a boolean, a named error turned into a 409, a 204 with no body, guards listed before the controller, a filter read from req.query, and async errors that Express 5 passes to the error handler.

            CHECK · predict before you choose

            Week 6’s deleteProduct repository function returns result.deletedCount === 1. Which layer turns false into a 404?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • The controller — The controller is the layer that speaks HTTP. It reads the boolean and chooses 404 or 204.
            • The repository, by calling res.status(404) — A repository has no res. It only talks to Mongo and returns plain values.
            • The error handler, because false is an error — Nothing is thrown. false is an ordinary answer that the controller reads.

            Build one layer at a time; let the checks lead

            1. Types first. Make the model types describe the new menu. npm run typecheck is your first test: it needs no database and no server, and passing it is worth 20 of the 120 points.
            2. The async change. Make the order validator async. The rules do not change. Typecheck should now pass.
            3. One layer at a time. The instructions suggest this order: the orders data layer, the two routers, placing and pricing an order, then menu CRUD. Finish a layer before you call it from the layer above.
            4. Run one suite while you work. npm test orders runs only the order suites, as in HW1, and npm run test:watch runs them again on every save. Push for autograder feedback sparingly, after npm test passes on your machine.
            5. Watch the data. Keep Compass open on your dev database while you send requests from Postman, and watch each order appear. npm run dev needs your local MongoDB (the Week 3 setup); npm test does not, because the tests start their own in-memory Mongo (MongoMemoryServer).

            Read the failure before you change code. A typecheck error names a file, a line and a code; read a red TypeScript error shows how to read each part. A failing test names the test, the expected value and the received value; read a Vitest failure walks through one. Decide which layer produced the wrong value, then open that layer.

            The instructions split the 120 points like this:

            What is gradedPoints
            The menu suite25
            The orders suite25
            The auth suite25
            The errors suite25
            npm run typecheck passes20
            Total120

            Partial credit within a suite follows the share of its tests you pass, so a suite that is mostly green still earns most of its 25. The deadline is on Canvas; this guide states none.

            In short

            Start with the types, because typecheck is free to run and worth 20 points. Then make validation async, build one layer at a time, run one suite at a time, and read each failure for the layer that caused it before you change code.

            CHECK · predict before you choose

            A test expects 404 for a well-formed id that matches nothing, and receives 500. Where do you look first?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • The token middleware — A token problem is a 401, and it stops the request before any lookup.
            • The model types — Types are erased before the code runs, so they cannot produce a status code.
            • The controller and the lookup it calls: something threw instead of returning null — A 500 is the error handler’s answer to an error it does not know. A missing document should come back as null and become a 404 in the controller.

            Be ready to explain it

            An instructor can ask these questions about any HW2 submission. Each answer comes from class code or a lesson in this guide, so you can practise them without opening anyone’s homework. Say each answer out loud before you read it.

            Which layer decides the status code?

            The controller. The repository returns a document, null or a boolean. The service returns values or throws a named error. The controller, or the error handler for a thrown error, turns that into HTTP. Week 4’s delete turns false into a 404 in the controller; Week 6’s error handler turns ConflictError into a 409 (give each layer one responsibility).

            Why validate before touching the database?

            A bad request should change nothing. Rejecting it first means no half-written document (the Week 4 memo bug shows what happens otherwise), and the code after validation can trust that what it looks up exists, as the instructions say about pricing.

            Why does the repository hide _id?

            So that Mongo stays inside one folder. Week 6’s toProduct turns _id into a string id, and the controllers, services and tests work with plain objects that never hold an ObjectId. If the database changed, one folder would change.

            What is the difference between an id’s shape and its existence?

            Shape is a question about the string: is it 24 hexadecimal characters? The id check answers it with a 400 before any lookup. Existence is a question for the database: does a document have that id? Only a lookup can answer it, and the controller answers 404 when the answer is no (an id’s shape and its existence are separate questions).

            Why 409 and not 400 for an id that is taken?

            A 400 says the request itself is wrong: fix it and it would work anywhere. A 409 says the request is fine but clashes with what is already stored: the same body would succeed on an empty database. Week 6 answers a taken username with 409 for the same reason (registering a user).

            Why is the total stored with the order?

            The instructions call an order a receipt: if prices change next week, the order still cost what it cost. Computing the total once, when the order is placed, keeps the past fixed.

            Next, Week 6 replaces the shared secrets with real accounts, which is what finally lets a customer read their own order.

            In short

            Each answer names a layer: the controller decides the status code, validation runs before the database so a bad request changes nothing, the repository hides _id so Mongo stays in one folder, shape is a 400 and existence is a 404, and a clash with stored data is a 409.

            CHECK · predict before you choose

            POST /menu sends a body with the right shape, but its id is already used. Which answer is right?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • 409, because the body is fine but clashes with an item that exists — The same body would succeed on an empty database. The problem is the clash with stored data.
            • 400, because the body is invalid — The body has the right shape. Fixing it would not change anything, because nothing in it is wrong.
            • 404, because the id was not found — The id was found, and that is the problem.

            Source trail · The public HW2 instructions, read October 14, 2026 and linked, not retained, and class exercise code from the Week 4 and Week 6 repos, quoted from pinned snapshots. No homework code appears.

            W6

            Week 6 · passwords, bcrypt, JWTs, roles, and ownership checks

            Know who is asking. Decide what they may do.

            Two questions: who are you, and may you do this?

            Week 6 adds user accounts to the Week 5 products API. “Auth” is short for two different jobs, and the code keeps them apart.

            Where this chapter sitsA client sends a request to the server. It passes route, middleware, controller and service; the service uses the database, and the response goes back to the client. Highlighted in this chapter: the middleware, which checks the token and the role, and the service, which checks who owns the thing being asked for.Clientbrowser, Postman, curlrequestresponseServerRouteMiddlewareControllerServiceDatabaseMiddleware: is the token valid?Is the role right?Service: does the caller own it?this chapter
            This chapter adds two checks: middleware asks who is calling and what role they have, and the service asks whether the thing belongs to them.

            Label the map · tap a name, then a slot

            Where each auth check runs

            Put each name where it belongs on the map.

            The chapter map with its names blankedThe same map as above, with 5 numbered slots where its names were.1?browser, Postman, curlrequestresponseServerRoute2?3?4?5?

            This exercise needs JavaScript. The map above shows every name in its place.

            JobThe questionIt fails whenStatus
            AuthenticateWho is making this request?There is no token, or the token does not verify401
            AuthorizeMay this caller do this?The token is valid, but the role is wrong or the caller is not the owner403

            HW1 and HW2 used shared secrets: one token for every staff member and one for every customer. Nobody had an account, so the server could not tell one customer from another, and it could not limit a customer to their own orders. Week 6 replaces the shared secret with a token issued to one user when they log in.

            HW1 read the same Authorization header, with a shared token in it.

            By the end of the week the API has five guarded or new routes:

            RouteGuardAnswers
            POST /auth/registerNone201 with the public user; 400 if a field is missing; 409 if the username is taken
            POST /auth/loginNone200 with a token; 401 for a wrong username or password
            GET /auth/meA valid token200 with the token’s payload; 401 otherwise
            POST /productsA valid token with the staff role201 for staff; 401 with no token or a bad one; 403 for a customer
            GET /users/:idA valid token, then an ownership check in the serviceStaff may read anyone; a customer may read only themselves; 403 otherwise; 404 if no user has that id

            In short

            Authentication asks who is calling and fails with 401. Authorization asks whether that caller may do this and fails with 403. Week 6 replaces the shared tokens of HW1 with one token per logged-in user.

            CHECK · predict before you choose

            A logged-in customer sends a valid token to POST /products, which only staff may use. Which status comes back?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • 401, because the customer is not authenticated — The token is valid, so the server knows exactly who this is.
            • 403, because the server knows who it is and the answer is no — Authentication passed and authorization failed, which is what 403 means.
            • 201, because any valid token is enough — A valid token proves identity only; this route also requires the staff role.

            Never store the password

            The one rule for passwords: never store the password itself. Store something a typed password can be checked against, and that is useless to whoever steals the database.

            A hash function turns its input into a fixed-size fingerprint, and it is one-way: the input cannot be worked back out of the output. To get a feel for it, imagine a very simple hash, the length of the word.

            InputOutput
            apple5
            banana6
            mango5

            Stealing the outputs tells an attacker very little, and typing apple at login gives 5 again, which matches. The toy also shows the weakness: mango gives 5 too. A real hash has so many possible outputs that a wrong input practically never matches.

            bcrypt is a hashing algorithm built for passwords. On top of the mathematics it adds two things.

            bcrypt addsWhat it doesWhat it defeats
            A saltA random value mixed in before hashing, so the same password hashes differently for every userA precomputed table of the hashes of common passwords, called a rainbow table
            A costIt is slow on purpose: cost 11 means 2048 rounds of workMillions of guesses; one login still takes a fraction of a second
            A bcrypt hash · from the Week 6 notes
            $2b$11$k.ywVRU10znZsfQTv5.zhOIrbnclw9eOuacaFbTQoVqKiGMfGhUEu
            \__/\/ \____________________/\_____________________________/
            alg cost        salt                      hash

            The cost and the salt are stored inside the output, so a later check needs nothing but the stored string.

            Hashing protects the password where it is stored. It does not protect the password on its way to the server: in class it travels over HTTP as plain text. A public server needs HTTPS for that, which the course reaches in a later week.

            In short

            Store a bcrypt hash, never the password. The hash is one-way, the salt makes equal passwords hash differently, and the cost makes guessing slow. Hashing does not protect a password in transit; HTTPS does.

            CHECK · predict before you choose

            Two users register with the same password. What does the users collection hold?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • The same hashedPassword string twice — Each hash gets its own random salt, so the two strings differ.
            • Two different hashedPassword strings — bcrypt mixes a fresh salt into each hash and stores it in the output.
            • The password, encrypted with JWT_SECRET — The password is hashed, not encrypted, and the JWT secret plays no part in it.

            Three shapes of one user

            The starter gives you the user types and the users repository, because they follow the products ones from Week 5.

            models/user.ts · from the Week 6 notes
            export type Role = 'customer' | 'staff';
            
            // A user as the app stores it. hashedPassword never leaves the server.
            export type User = {
              id: string;
              username: string;
              hashedPassword: string;
              role: Role;
            };
            
            // What the repository needs to create one: everything but the id.
            export type NewUser = {
              username: string;
              hashedPassword: string;
              role: Role;
            };
            
            // A user as the outside world sees it.
            export type PublicUser = {
              id: string;
              username: string;
              role: Role;
            };
            TypeFieldsUsed for
            Userid, username, hashedPassword, roleWhat the repository hands back
            NewUserEverything but the idWhat the repository needs to create a user
            PublicUserid, username, roleThe only shape that may leave the server

            Product and NewProduct were the same two-shape pattern. PublicUser is the new idea: a response carries it and nothing more. An attacker cannot do much with a hashed password, but nothing good comes of passing it around, so it stays inside the backend and is used for logging in only.

            db/users-repository.ts · from the Week 6 notes
            const toUser = (doc: Document): User => {
              return {
                id: doc._id.toString(),
                username: doc.username,
                hashedPassword: doc.hashedPassword,
                role: doc.role
              };
            };
            
            export const findUserByUsername = async (
              username: string
            ): Promise<User | null> => {
              const doc = await getDb().collection(USERS).findOne({ username });
              return doc ? toUser(doc) : null;
            };
            
            export const findUser = async (id: string): Promise<User | null> => {
              const doc = await getDb()
                .collection(USERS)
                .findOne({ _id: new ObjectId(id) });
              return doc ? toUser(doc) : null;
            };

            The repository does what the products one does. Its toUser function turns a MongoDB document into a User, so _id never leaves the database layer. Note that findUser builds an ObjectId from the id, which matters again when you unit test the service.

            In short

            User is what is stored, NewUser is a User without an id, and PublicUser is a User without the hashed password. Only a PublicUser goes into a response.

            CHECK · predict before you choose

            Which type may a controller send back in a response?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • User — User carries hashedPassword, which never leaves the server.
            • NewUser — NewUser also carries hashedPassword, and it has no id yet.
            • PublicUser — It holds the id, the username and the role, and no password field at all.

            Register: check, hash, save

            POST /auth/register takes a username and a password and creates a user. The service does three things in order.

            services/auth-service.ts · from the Week 6 notes
            import bcrypt from 'bcryptjs';
            import jwt from 'jsonwebtoken';
            import * as usersRepository from '../db/users-repository.js';
            import { getJwtSecret } from '../config.js';
            import {
              AuthenticationError,
              ConflictError,
              ValidationError
            } from '../errors.js';
            import type { PublicUser } from '../models/user.js';
            
            const SALT_ROUNDS = 11;
            
            export const registerUser = async (
              username: string,
              password: string
            ): Promise<PublicUser> => {
              if (!username || !password) {
                throw new ValidationError('username and password are required');
              }
            
              // 1. is the username free?
              const existing = await usersRepository.findUserByUsername(username);
              if (existing) {
                throw new ConflictError('That username is taken');
              }
            
              // 2. hash the password
              const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
            
              // 3. save the user -- everyone who registers is a customer
              const user = await usersRepository.insertUser({
                username,
                hashedPassword,
                role: 'customer'
              });
            
              return { id: user.id, username: user.username, role: user.role };
            };
            SituationThe serviceResponse
            The username or the password is missingThrows a ValidationError400
            The username is already takenThrows a ConflictError409
            OtherwiseHashes the password, inserts a customer, returns a PublicUser201

            The new error classes follow the Week 5 pattern: each is a thin subclass of Error whose only job is to be recognisable.

            errors.ts · from the Week 6 notes
            export class ValidationError extends Error {}
            
            // 409: the request clashes with something that already exists.
            export class ConflictError extends Error {}
            
            // 401: we don't know who you are -- no credentials, or bad ones.
            export class AuthenticationError extends Error {}
            
            // 403: we know who you are, and you're not allowed to do this.
            export class AuthorizationError extends Error {}
            middleware/error-handler.ts · from the Week 6 notes
            import type { NextFunction, Request, Response } from 'express';
            import {
              AuthenticationError,
              AuthorizationError,
              ConflictError,
              ValidationError
            } from '../errors.js';
            
            export const errorHandler = (
              err: Error,
              req: Request,
              res: Response,
              next: NextFunction
            ): void => {
              if (err instanceof ValidationError) {
                res.status(400).json({ error: err.message });
                return;
              }
              if (err instanceof AuthenticationError) {
                res.status(401).json({ error: err.message });
                return;
              }
              if (err instanceof AuthorizationError) {
                res.status(403).json({ error: err.message });
                return;
              }
              if (err instanceof ConflictError) {
                res.status(409).json({ error: err.message });
                return;
              }
              res.status(500).json({ error: 'Something went wrong' });
            };

            A taken username is not a malformed request and it is not the server’s fault. It clashes with something that already exists, which HTTP calls 409 Conflict.

            Week 5 introduced this error handler with one error type.

            controllers/auth-controller.ts · from the Week 6 notes
            export const postRegister = async (
              req: Request,
              res: Response
            ): Promise<void> => {
              const user = await authService.registerUser(
                req.body?.username,
                req.body?.password
              );
              res.status(201).json(user);
            };

            The controller passes on two fields and nothing else. req.body?.username gives undefined, not a crash, when a request has no body, so the service’s own check answers 400.

            Everyone who registers is a customer. The role is written by the service, never read from the request, so a client cannot register itself as staff.

            In short

            registerUser checks the fields (400), checks the username is free (409), hashes the password with bcrypt, and saves a customer. It returns a PublicUser, and the controller answers 201.

            CHECK · predict before you choose

            A client posts the username ada, a password, and role staff to /auth/register. The name is free. What is stored?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • A customer named ada — The controller passes only username and password, and the service always writes the customer role.
            • A staff user named ada — The role in the body is never read; staff accounts come only from the seed script.
            • Nothing; the extra field is a 400 — The service validates only that username and password are present; an extra field is ignored.

            Log in: compare, then hand back a token

            POST /auth/login checks a username and a password. This is the finished function; the role in the payload is explained in the roles lesson.

            services/auth-service.ts · from the Week 6 notes
            export const logIn = async (
              username: string,
              password: string
            ): Promise<string> => {
              const user = await usersRepository.findUserByUsername(username);
              if (!user) {
                throw new AuthenticationError('Wrong username or password');
              }
            
              const matches = await bcrypt.compare(password, user.hashedPassword);
              if (!matches) {
                throw new AuthenticationError('Wrong username or password');
              }
            
              return jwt.sign(
                { sub: user.id, username: user.username, role: user.role },
                getJwtSecret(),
                { expiresIn: '1h' }
              );
            };

            bcrypt.compare reads the salt and the cost out of the stored hash, hashes the typed password the same way, and resolves to true or false. The plain password is never stored and never compared directly.

            Both failures throw the same AuthenticationError with the same message. Saying which half was wrong would tell an attacker which usernames exist.

            The request hasStatusBody
            The right username and password200{ token }
            A username nobody has401Wrong username or password
            The right username, the wrong password401Wrong username or password

            Why hand back anything? HTTP requests are stateless: once this request ends, the server remembers nothing about the login. So the server gives the client a token that proves who the user is, and the client sends it with every later request.

            controllers/auth-controller.ts · from the Week 6 notes
            export const postLogin = async (
              req: Request,
              res: Response
            ): Promise<void> => {
              const token = await authService.logIn(
                req.body?.username,
                req.body?.password
              );
              res.json({ token });
            };

            In short

            logIn finds the user, lets bcrypt.compare check the password against the stored hash, and signs a token. A wrong username and a wrong password get the same 401 and the same message.

            CHECK · predict before you choose

            Someone tries to log in with a username that does not exist. What does the response say?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • 404, no user with that username — That would confirm which usernames exist, which is what the shared message avoids.
            • 400, the username is invalid — 400 is for a request with missing or malformed data; this one is well formed.
            • 401, wrong username or password — Both failures throw the same AuthenticationError, which the error handler maps to 401.

            A JWT: anyone can read it, only the server can make it

            A JSON Web Token has three parts separated by dots: header, payload, signature.

            A token from login · from the Week 6 notes
            eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI2NjUwYTFjMmU0YjBmM2E5ZDFjMmIzYTQiLCJ1c2VybmFtZSI6ImFkYSIsImlhdCI6MTc5MTY4OTk5MiwiZXhwIjoxNzkxNjkzNTkyfQ.FkWpv-Lu9SI1HzD4bCSbVrKYshbsOGWi_sxfbWhqQrg
            PartDecodedIts job
            Header{"alg":"HS256","typ":"JWT"}The kind of token, and how it is signed
            Payload{"sub":"6650a1c2e4b0f3a9d1c2b3a4","username":"ada","iat":1791689992,"exp":1791693592}Who the token is about; each field is called a claim
            SignatureNot JSONComputed from the header, the payload and a secret only the server knows

            The header and the payload do not look like JSON because they are base64url-encoded. That is a translation, not a lock: there is no key, and anyone can turn it back. Tokens travel in HTTP headers, where the quotes, braces and spaces of JSON would cause trouble.

            base64 · from the Week 6 notes
            Hi!   →   SGkh
            ClaimMeaning
            subSubject: whose token this is. Here, the user’s id
            iatIssued at: when the token was signed
            expExpires: when the token stops being accepted
            username, roleClaims this app adds for its own use

            So a payload is public. Never put a password or any other secret in it. What protects a token is the signature: change one character of the header or the payload and the signature no longer matches, and only someone who has the secret can compute a matching one.

            jwt.io decodes a token in the browser ↗

            Paste a token from your own login there to read its payload. Do not paste a secret that a real system uses.

            In short

            A JWT is header.payload.signature. The first two parts are only encoded, so anyone can read them. The signature needs the server’s secret, so nobody else can change a token or make a new one.

            CHECK · predict before you choose

            Someone copies a customer’s token but does not have the server’s secret. What can they not do?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • Change the role to staff and have the server accept it — Editing the payload breaks the signature, and a new signature needs the secret.
            • Read the username in the payload — The payload is only base64url-encoded, so anyone can decode it.
            • Find out when the token expires — The exp claim sits in the readable payload.

            The secret lives in the environment

            Signing needs a secret, and secrets do not go in code, because code goes to GitHub. They go in the environment: a set of variables that live outside the code and that the program reads when it runs.

            FileIn git?Holds
            .env.exampleYesA placeholder and instructions
            .envNo; .gitignore excludes itYour real JWT_SECRET
            vitest.config.tsYesA fixed secret that only the tests use
            Terminal · from the Week 6 notes
            cp .env.example .env
            
            node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

            The second command prints 32 random bytes as text. Put that value in .env, keeping the line’s shape exactly: the name, an equals sign with no spaces around it, and the value in quotes.

            .env · from the Week 6 notes
            JWT_SECRET="a23ae72558e4fafd21dcdfcf563edbf5a54ea69c027617d282af3d3dc4dbab32"

            Node reads .env only when told to. The scripts in package.json pass the flag:

            package.json · from the Week 6 notes
            "dev": "tsx watch --env-file=.env src/index.ts",
            "start": "node --env-file=.env dist/index.js",
            config.ts · from the Week 6 notes
            // Settings that come from the environment rather than from the code.
            //
            // `npm run dev` and `npm start` load .env with Node's --env-file flag (see
            // package.json). The tests set their own value in vitest.config.ts.
            
            export const getJwtSecret = (): string => {
              const secret = process.env.JWT_SECRET;
              if (!secret) {
                throw new Error('JWT_SECRET is not set. Copy .env.example to .env.');
              }
              return secret;
            };

            getJwtSecret stops with a clear message when the variable is missing. That is better than signing every token with undefined.

            Tests never read .env: it is not in the repository, so a fresh clone or the autograder has none. They get a fixed secret from the Vitest configuration.

            vitest.config.ts · from the Week 6 notes
            import { defineConfig } from 'vitest/config';
            
            export default defineConfig({
              test: {
                hookTimeout: 120_000,
                testTimeout: 20_000,
                // Tests never read .env -- it isn't in the repo, so a fresh clone or
                // the autograder wouldn't have one. They use this fixed secret instead.
                env: {
                  JWT_SECRET: 'test-secret'
                }
              }
            });

            Watch mode reloads the app when code changes, and .env is not code. After you edit .env, stop the dev server with Ctrl-C and start it again.

            Older projects and many tutorials load .env with a package named dotenv, from the time when Node could not read the file itself. The syntax differs and the result is the same.

            In short

            JWT_SECRET lives in .env, which git ignores; .env.example is the committed template. Node loads it with --env-file, config.ts reads it, and tests use a fixed secret from vitest.config.ts. Restart the dev server after changing .env.

            CHECK · predict before you choose

            You change JWT_SECRET in .env while npm run dev is running. Tokens signed with the old secret still work. Why?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • Verifying a token does not use the secret — jwt.verify recomputes the signature with the secret; that is the whole check.
            • The watcher reloads on code changes only, so the server still holds the old value — The environment is read when the process starts; restart the server to load the new secret.
            • vitest.config.ts overrides .env while the server runs — That configuration applies to test runs only, not to npm run dev.

            requireJwt: check the token before anything else

            A route that needs a logged-in user has to check the token before anything else happens. That is a job for middleware.

            middleware/require-jwt.ts · from the Week 6 notes
            import type { NextFunction, Request, Response } from 'express';
            import jwt from 'jsonwebtoken';
            import { getJwtSecret } from '../config.js';
            import { AuthenticationError } from '../errors.js';
            
            export const requireJwt = (
              req: Request,
              res: Response,
              next: NextFunction
            ): void => {
              const header = req.headers.authorization;
              if (!header || !header.startsWith('Bearer ')) {
                throw new AuthenticationError('No token');
              }
              const token = header.slice('Bearer '.length);
            
              try {
                res.locals.user = jwt.verify(token, getJwtSecret());
              } catch {
                throw new AuthenticationError('Invalid or expired token');
              }
            
              next();
            };
            1. Read the Authorization header. If it is missing or does not start with Bearer , throw an AuthenticationError.
            2. Slice the prefix off. What is left is the token.
            3. jwt.verify recomputes the signature with the secret and checks the exp claim. If either check fails it throws, and the catch block throws an AuthenticationError in its place.
            4. On success verify returns the payload. Store it in res.locals.user, the place Express provides for middleware to leave data for later handlers.
            5. Call next() so the request moves on.

            The error handler turns every AuthenticationError into 401, so the middleware never touches res.status itself.

            The simplest protected route tells the caller who the token says they are.

            controllers/auth-controller.ts · from the Week 6 notes
            export const getMe = (req: Request, res: Response): void => {
              res.json(res.locals.user);
            };
            routes/auth-router.ts · from the Week 6 notes
            import express from 'express';
            import * as authController from '../controllers/auth-controller.js';
            import { requireJwt } from '../middleware/require-jwt.js';
            
            export const authRouter = express.Router();
            
            authRouter.post('/register', authController.postRegister);
            authRouter.post('/login', authController.postLogin);
            authRouter.get('/me', requireJwt, authController.getMe);

            Middleware is named in the route, before the controller. Only /me lists requireJwt, so register and login stay open to everyone, as they must.

            In Postman, choose Bearer Token on the Authorization tab and paste the token. Postman writes the header for you.

            In short

            requireJwt reads the Bearer token from the Authorization header, verifies its signature and expiry, and stores the payload in res.locals.user. Anything wrong becomes an AuthenticationError, which the error handler answers with 401.

            CHECK · predict before you choose

            A request arrives with the header Authorization: abc123, with no Bearer prefix. What happens?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • jwt.verify throws, and the answer is 401 Invalid or expired token — The prefix check fails first, so verify never runs.
            • The answer is 401 No token, and jwt.verify is never called — The header does not start with the Bearer prefix, so the first check throws.
            • next() runs, and the controller finds no user — The middleware throws before it reaches next, so the controller never runs.

            Attack your own tokens

            A test can sign its own token, because the tests know the test secret. That lets a test present tokens that login would never hand out.

            __tests__/auth.e2e.test.ts · from the Week 6 notes
            const payload = { sub: '650000000000000000000001', username: 'ada' };
            
            it('200s with the username for a token we sign ourselves', async () => {
              const token = jwt.sign(payload, getJwtSecret());
            
              const res = await request(app)
                .get('/auth/me')
                .set('Authorization', `Bearer ${token}`);
            
              expect(res.status).toBe(200);
              expect(res.body.username).toBe('ada');
            });
            The tokenHow the test makes itExpect
            No tokenLeave the Authorization header off401
            Not a JWTSend Bearer not-a-token401
            Signed with the wrong secretjwt.sign(payload, 'not-the-secret')401
            ExpiredSign with { expiresIn: -10 }401
            Edited payloadRe-encode a changed payload and keep the original signature401
            From a real loginRegister, log in, and use the token that comes back200 and the username
            __tests__/auth.e2e.test.ts · composed for this guide, run with Vitest 5.0 against the Week 6 starter
            // A negative expiresIn means it expired ten seconds ago.
            it('401s for a token that has expired', async () => {
              const token = jwt.sign(payload, getJwtSecret(), { expiresIn: -10 });
            
              const res = await request(app)
                .get('/auth/me')
                .set('Authorization', `Bearer ${token}`);
            
              expect(res.status).toBe(401);
            });
            __tests__/auth.e2e.test.ts · composed for this guide, run with Vitest 5.0 against the Week 6 starter
            // Anyone can read and rewrite the payload, but the signature was made
            // from the original one, so it no longer matches.
            it('401s for a token whose payload has been edited', async () => {
              const [header, body, signature] = jwt
                .sign(payload, getJwtSecret())
                .split('.');
              const claims = JSON.parse(Buffer.from(body, 'base64url').toString());
              claims.username = 'mallory';
              const forgedBody = Buffer.from(JSON.stringify(claims)).toString(
                'base64url'
              );
            
              const res = await request(app)
                .get('/auth/me')
                .set('Authorization', `Bearer ${header}.${forgedBody}.${signature}`);
            
              expect(res.status).toBe(401);
            });

            The edited-payload test is the one that shows what a signature is for. It splits a good token at the dots, decodes the middle part, changes the username, encodes it again, and joins it to the original header and signature. The server recomputes the signature from what it received, and it no longer matches.

            __tests__/auth.e2e.test.ts · composed for this guide, run with Vitest 5.0 against the Week 6 starter
            // The only test here that needs the database.
            it('200s with the username for a token from a real login', async () => {
              const credentials = { username: 'ada', password: 'correct-horse' };
              await request(app).post('/auth/register').send(credentials);
              const login = await request(app).post('/auth/login').send(credentials);
            
              const res = await request(app)
                .get('/auth/me')
                .set('Authorization', `Bearer ${login.body.token}`);
            
              expect(res.status).toBe(200);
              expect(res.body.username).toBe('ada');
            });

            The first five tests try the lock. The last one tries the key that login hands out, and it is the only one that needs the database.

            In short

            Sign tokens in the test to try every way a token can be bad: missing, not a JWT, wrong secret, expired, or edited. Each must get 401. One more test registers and logs in for real and must get 200.

            CHECK · predict before you choose

            requireJwt is changed to call jwt.decode, which reads the payload without checking the signature. Which test still passes?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • 401s for a token signed with the wrong secret — decode accepts any well-formed token, so this request would get 200 and the test would fail.
            • 401s for a token whose payload has been edited — Nothing compares the payload with the signature any more, so the edit goes unnoticed.
            • 200s with the username for a token from a real login — A good token still decodes to the right username, which is why this test alone proves little.

            Roles: requireRole runs after requireJwt

            Being logged in is not enough to change the catalogue. Creating a product is a staff job. Deciding what a caller may do from their role is called role-based access control, or RBAC.

            Registration only ever makes customers, so the starter has a script that creates one staff user in your local database. Run npm run seed once, with MongoDB running. It creates the user staff with the password staff-password, for local development only.

            Login copies the role into the token’s payload, so a later request can be checked without another trip to the database. A token issued before that change carries no role: log in again to get one that does.

            middleware/require-role.ts · from the Week 6 notes
            import type { NextFunction, Request, Response } from 'express';
            import { AuthorizationError } from '../errors.js';
            import type { Role } from '../models/user.js';
            
            type Middleware = (req: Request, res: Response, next: NextFunction) => void;
            
            // Not middleware itself: a function that MAKES middleware for one role.
            // Always runs after requireJwt, which has put the token's payload in
            // res.locals.user.
            export const requireRole = (role: Role): Middleware => {
              return (req: Request, res: Response, next: NextFunction): void => {
                if (res.locals.user.role !== role) {
                  throw new AuthorizationError(`Only ${role} may do this`);
                }
                next();
              };
            };

            requireRole is not middleware. It is a function that makes middleware for one role. requireRole('staff') runs once, when the router file loads, and the function it returns runs on every request.

            routes/products-router.ts · from the Week 6 notes
            import express from 'express';
            import * as productsController from '../controllers/products-controller.js';
            import { requireJwt } from '../middleware/require-jwt.js';
            import { requireRole } from '../middleware/require-role.js';
            
            export const productsRouter = express.Router();
            
            productsRouter.get('/', productsController.getProducts);
            productsRouter.get('/:id', productsController.getProduct);
            productsRouter.post(
              '/',
              requireJwt,
              requireRole('staff'),
              productsController.postProduct
            );
            productsRouter.patch('/:id', productsController.patchProduct);
            productsRouter.delete('/:id', productsController.deleteProduct);

            Rebuild it · put the lines in order

            Put the guards in order

            Rebuild the route that lets only staff create a product.

            Show the solved code
            productsRouter.post(
              '/',
              requireJwt,
              requireRole('staff'),
              productsController.postProduct
            );

            Express runs a route’s handlers in the order they are listed. requireJwt works out who is calling, requireRole reads that answer, and the controller runs only when both have let the request through.

            Two gates before the controllerA POST /products request meets three handlers from left to right. requireJwt sends 401 Unauthorized when there is no token or a bad one. requireRole sends 403 Forbidden when the token is valid but the role is wrong. Only a staff token reaches the controller, which answers 201 Created.POST /productshandlers run left to rightrequireJwtwho are you?requireRoleallowed?Controllercreates itno token,or a bad onewrong rolestaff token401Unauthorized403Forbidden201Created
            Notice which gate answers: 401 comes from requireJwt and 403 from requireRole, and the controller never runs for either.
            The request hasStatusMeansWho says so
            No token, or a bad one401 UnauthorizedWe do not know who you arerequireJwt
            A valid token, the wrong role403 ForbiddenWe know who you are, and the answer is norequireRole
            A valid staff token201 CreatedGo aheadThe products controller

            The order is part of the behavior. requireRole reads res.locals.user.role, which exists only after requireJwt has stored the payload. With the two swapped, res.locals.user is undefined, reading its role throws a TypeError, and the error handler answers 500 to everyone, staff included.

            Week 2 showed that middleware order is part of the behavior.

            The Week 5 tests that post a product now fail with 401, because the app does what it was told. The tests know the test secret, so they sign two tokens once, near the top of the file.

            __tests__/products.e2e.test.ts · from the Week 6 notes
            // POST /products needs a staff token now -- and one customer token, to
            // check that a customer is turned away.
            const staffToken = jwt.sign(
              { sub: '650000000000000000000001', username: 'tester', role: 'staff' },
              getJwtSecret()
            );
            const customerToken = jwt.sign(
              { sub: '650000000000000000000002', username: 'ada', role: 'customer' },
              getJwtSecret()
            );
            
              it('401s without a token', async () => {
                const res = await request(app)
                  .post('/products')
                  .send({ modelName: 'Sneaky' });
            
                expect(res.status).toBe(401);
              });
            
              it('403s for a customer', async () => {
                const res = await request(app)
                  .post('/products')
                  .set('Authorization', `Bearer ${customerToken}`)
                  .send({ modelName: 'Sneaky' });
            
                expect(res.status).toBe(403);
              });

            The tests that should succeed send the staff token the same way. The optional tasks put the same two guards on PATCH and DELETE, and then six Week 5 PATCH tests need the staff token too.

            In short

            Login puts the role in the token. requireRole(role) returns middleware that throws an AuthorizationError, answered with 403, when the token’s role differs. It must come after requireJwt, which is what puts the payload in res.locals.user.

            CHECK · predict before you choose

            On POST /products, requireRole is listed before requireJwt. A staff user sends a valid token. What comes back?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • 201, because the token is valid and the role is staff — requireRole runs first and finds no user in res.locals, so the request never gets that far.
            • 403, because requireRole cannot see a role — Reading the role of an undefined user throws a TypeError, not an AuthorizationError.
            • 500, because reading the role throws an unexpected error — res.locals.user is still undefined, and the error handler maps any unknown error to 500.

            Ownership: a rule that roles cannot express

            Some rules depend on who owns the thing being asked for. A customer may read their own profile and nobody else’s; staff may read anyone’s. A role check cannot say that, because the two customers have the same role.

            services/users-service.ts · from the Week 6 notes
            import * as usersRepository from '../db/users-repository.js';
            import { AuthorizationError } from '../errors.js';
            import type { PublicUser, Role } from '../models/user.js';
            
            // Who is asking, as far as their token says.
            type Caller = {
              id: string;
              role: Role;
            };
            
            export const getUser = async (
              caller: Caller,
              id: string
            ): Promise<PublicUser | null> => {
              // Staff may read anyone. Everyone else may read only themselves.
              if (caller.role !== 'staff' && caller.id !== id) {
                throw new AuthorizationError('You can only see your own profile');
              }
            
              const user = await usersRepository.findUser(id);
              if (!user) {
                return null;
              }
              return { id: user.id, username: user.username, role: user.role };
            };

            The rule lives in the service, where policy and business rules belong. For this one route it could live in middleware, because everything it needs is in the token and the URL. That is usually not true. To decide whether an order belongs to the caller, the order has to be loaded first and its customerId compared with the caller’s id.

            controllers/users-controller.ts · from the Week 6 notes
            import type { Request, Response } from 'express';
            import * as usersService from '../services/users-service.js';
            
            export const getUser = async (req: Request, res: Response): Promise<void> => {
              const caller = { id: res.locals.user.sub, role: res.locals.user.role };
              const user = await usersService.getUser(caller, String(req.params.id));
              if (!user) {
                res.status(404).json({ error: 'No user with that id' });
                return;
              }
              res.json(user);
            };

            The controller builds the caller from the token’s payload: sub becomes the id, and the role is copied. The service receives a plain object and knows nothing about tokens or HTTP, which is what lets a unit test call it directly.

            routes/users-router.ts · from the Week 6 notes
            usersRouter.get('/:id', requireJwt, usersController.getUser);
            CallerAsks forResult
            The customer adaHer own profile200
            The customer adaThe profile of grace403
            StaffThe profile of grace200
            StaffAn id that no user has404
            Nobody: no tokenAny profile401

            The end-to-end tests insert ada, grace and staff before every test, then sign a token with each user’s real id as the sub. They never log in, so the placeholder hashed passwords are never checked.

            __tests__/users.e2e.test.ts · from the Week 6 notes
            it("403s for a customer reading someone else's profile", async () => {
              const token = jwt.sign(
                { sub: adaId, username: 'ada', role: 'customer' },
                getJwtSecret()
              );
            
              const res = await request(app)
                .get(`/users/${graceId}`)
                .set('Authorization', `Bearer ${token}`);
            
              expect(res.status).toBe(403);
            });
            __tests__/users.e2e.test.ts · composed for this guide, run with Vitest 5.0 against the Week 6 starter
            it('200s for a customer reading their own profile', async () => {
              const token = jwt.sign(
                { sub: adaId, username: 'ada', role: 'customer' },
                getJwtSecret()
              );
            
              const res = await request(app)
                .get(`/users/${adaId}`)
                .set('Authorization', `Bearer ${token}`);
            
              expect(res.status).toBe(200);
              expect(res.body.username).toBe('ada');
            });
            
            it("200s for staff reading anyone's profile", async () => {
              const token = jwt.sign(
                { sub: staffId, username: 'staff', role: 'staff' },
                getJwtSecret()
              );
            
              const res = await request(app)
                .get(`/users/${graceId}`)
                .set('Authorization', `Bearer ${token}`);
            
              expect(res.status).toBe(200);
              expect(res.body.username).toBe('grace');
            });

            In short

            An ownership rule compares the caller with the thing asked for: staff may read anyone, everyone else only themselves. It lives in the service, because most ownership checks need data from the database. The route needs only requireJwt.

            CHECK · predict before you choose

            GET /orders/:id must answer only the customer who placed the order. Where can that check run?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • In the service, after the order has been loaded — The owner is stored on the order, so the order must be read before the caller can be compared with it.
            • In requireRole, with the customer role — Every customer has the customer role, so a role cannot tell one customer’s order from another’s.
            • In middleware, from the token alone — The token says who is calling, not who placed the order; that is in the database.

            Check first, then look up

            The end-to-end test proves that a customer cannot read someone else’s profile. It cannot prove how the service said no, and that turns out to matter.

            services/users-service.unit.test.ts · from the Week 6 notes
            import { beforeEach, describe, expect, it, vi } from 'vitest';
            import * as usersService from './users-service.js';
            import * as usersRepository from '../db/users-repository.js';
            import { AuthorizationError } from '../errors.js';
            import type { User } from '../models/user.js';
            
            beforeEach(() => {
              vi.restoreAllMocks();
            });
            
            // A user as the repository hands it back -- hashed password and all.
            const grace: User = {
              id: 'grace-id',
              username: 'grace',
              hashedPassword: '$2b$11$not-a-real-hash',
              role: 'customer'
            };
            
            it('never looks the user up when a customer asks for someone else', async () => {
              const findUser = vi
                .spyOn(usersRepository, 'findUser')
                .mockResolvedValue(grace);
            
              await expect(
                usersService.getUser({ id: 'ada-id', role: 'customer' }, 'grace-id')
              ).rejects.toThrow(AuthorizationError);
            
              expect(findUser).not.toHaveBeenCalled();
            });

            The spy on findUser does three jobs: no database is needed, the test decides what the lookup returns, and afterwards the test can ask whether the lookup happened at all. Ids such as grace-id are fine here, because the real repository, which needs a 24-character ObjectId, never runs.

            Week 5 introduced vi.spyOn and mockResolvedValue.

            Here is a version of getUser that looks just as reasonable: look the user up first, then check.

            services/users-service.ts, the two steps swapped · from the Week 6 notes
              const user = await usersRepository.findUser(id);
              if (!user) {
                return null;
              }
              // Staff may read anyone. Everyone else may read only themselves.
              if (caller.role !== 'staff' && caller.id !== id) {
                throw new AuthorizationError('You can only see your own profile');
              }
            VersionEnd-to-end 403 testUnit testWhat a customer can learn
            Check, then look upPassesPassesNothing: every other id answers 403
            Look up, then checkPassesFails: findUser was calledWhich ids exist: 404 for a missing id, 403 for a real one

            The swapped version answers a missing id before it checks whether the caller was allowed to ask. Any customer could then probe ids and learn which ones belong to real users. The end-to-end test stays green because the response for a real user is still 403. The unit test goes red because it asks the one question that separates the two versions: was the database touched?

            The remaining four unit tests pin down the rest of the function’s contract.

            services/users-service.unit.test.ts · composed for this guide, run with Vitest 5.0 against the Week 6 starter
            it('lets staff read anyone, and looks up exactly that id', async () => {
              const findUser = vi
                .spyOn(usersRepository, 'findUser')
                .mockResolvedValue(grace);
            
              const user = await usersService.getUser(
                { id: 'staff-id', role: 'staff' },
                'grace-id'
              );
            
              expect(user?.username).toBe('grace');
              expect(findUser).toHaveBeenCalledWith('grace-id');
            });
            
            it('lets a customer read themselves', async () => {
              vi.spyOn(usersRepository, 'findUser').mockResolvedValue(grace);
            
              const user = await usersService.getUser(
                { id: 'grace-id', role: 'customer' },
                'grace-id'
              );
            
              expect(user).toEqual({ id: 'grace-id', username: 'grace', role: 'customer' });
            });
            
            it('never returns the hashed password', async () => {
              vi.spyOn(usersRepository, 'findUser').mockResolvedValue(grace);
            
              const user = await usersService.getUser(
                { id: 'staff-id', role: 'staff' },
                'grace-id'
              );
            
              expect(user).not.toHaveProperty('hashedPassword');
            });
            
            it("returns null for a user that doesn't exist", async () => {
              vi.spyOn(usersRepository, 'findUser').mockResolvedValue(null);
            
              const user = await usersService.getUser(
                { id: 'staff-id', role: 'staff' },
                'nobody-id'
              );
            
              expect(user).toBeNull();
            });
            To check thatUse
            The service looked up exactly the id that was asked fortoHaveBeenCalledWith('grace-id')
            A user that does not exist gives nullmockResolvedValue(null)
            The hashed password never leaves the servicenot.toHaveProperty('hashedPassword')
            A refused caller caused no lookupnot.toHaveBeenCalled()

            In short

            Authorize before touching the database. Looking up first leaks which ids exist, and an end-to-end test cannot see the difference. A unit test can: it spies on findUser and asserts it was never called.

            CHECK · predict before you choose

            getUser is rewritten to look the user up before checking the caller. The customer ada asks for an id that no user has. What does she get?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • 403, the same as for any id that is not hers — That is the correct version; the rewritten one returns null before it reaches the check.
            • 404, which tells her that no user has that id — The lookup runs first and returns null, so the controller sends 404 and the leak is open.
            • 401, because her token cannot be used for that id — Her token is valid, so requireJwt lets the request through.

            What this week does not cover

            The techniques here are solid when used correctly, and they are a starting point. Know where they stop.

            GapWhat it means in practice
            Passwords cross HTTP as plain textAnyone who can watch the network can read them until the server uses HTTPS
            A token cannot be taken backA stolen token works until its exp time; there is no server-side logout
            The role is copied into the token at loginA user who is demoted keeps the old role until the token expires
            No refreshAfter one hour the user has to log in again
            No reset, no email check, no second factorA forgotten or guessed password has no remedy
            Two roles onlyThere is no admin, and no table of which role may do which action

            For these reasons most production apps use an auth library or a provider, such as Auth0, Logto or Firebase, which is kept patched against new attacks. The ideas carry over unchanged: authentication and authorization, tokens, roles, and ownership checks appear in every one of them.

            Status codes for auth also vary between assignments. The Week 2 class code sent 403 for a bad token and HW1 required 401 even for the wrong role. Week 6 uses 401 when the caller is unknown and 403 when the caller is known and refused. Read the contract of each assignment before you assume.

            In short

            This week’s auth has no HTTPS, no revocation, no refresh and no password reset, and a role change takes effect only when the old token expires. Production apps usually rely on an auth provider; the concepts are the same.

            CHECK · predict before you choose

            A staff user is changed to customer in the database at 2:00. Their token was issued at 1:30 and lasts one hour. At 2:15 they send POST /products. What happens?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • 403, because the database now says customer — requireRole reads the role from the token and never asks the database.
            • 201, because the token still says staff until 2:30 — The role was copied into the token at login, and the token is still valid.
            • 401, because the old token was cancelled — Nothing in this design can cancel a token before it expires.

            Say it, then try it

            Answer each question aloud first, then open it.

            1. Authentication or authorization: which one answers 403?

            Authorization. The server knows who is calling and the answer is no. Authentication failures answer 401.

            2. What does the users collection store in place of a password?

            A bcrypt hash, which carries its own salt and cost.

            3. What does the salt stop?

            Two users with the same password having the same hash, and with it the use of precomputed tables of hashes.

            4. Why does registration answer 409 for a taken username, not 400?

            The request is well formed. It clashes with something that already exists, which is what Conflict means.

            5. Why do a wrong username and a wrong password get the same message?

            A different message would tell an attacker which usernames exist.

            6. Can someone without the secret read the payload of a JWT?

            Yes. It is only base64url-encoded. The secret is needed to sign, not to read.

            7. Which two things does jwt.verify check?

            That the signature matches the header and payload, and that the exp time has not passed.

            8. Where does requireJwt leave the payload for later handlers?

            In res.locals.user.

            9. Why do the tests not read .env?

            It is not in the repository, so a fresh clone or the autograder has none. vitest.config.ts sets a fixed test secret.

            10. Why is requireRole a function that returns middleware?

            So one piece of code can guard any role: the role is fixed when the route is defined.

            11. Why does the ownership check live in the service?

            Most ownership checks need the stored record, such as an order’s customerId, and policy belongs in the service.

            12. Which assertion proves a refused caller never reached the database?

            expect(findUser).not.toHaveBeenCalled(), on a spy placed on the repository function.

            The tasks come from the two Now You Try activities and their optional parts.

            Task A. Write the test for a token signed with the wrong secret.

            Sign the payload with any other string, send it as a Bearer token to GET /auth/me, and expect 401.

            Task B. Lock down DELETE /products/:id so that only staff may delete.

            Put requireJwt and then requireRole with the staff role before the controller. Test 401 with no token, 403 with the customer token, and 204 with the staff token followed by a GET that answers 404.

            Task C. Lock down PATCH /products/:id the same way. Six Week 5 tests turn red. Why, and what fixes them?

            They send no token, so requireJwt answers 401 before the controller runs. Set the Authorization header to the staff token on each PATCH request.

            In short

            Hash passwords, sign a token at login, verify it in middleware, check the role in middleware and ownership in the service, and authorize before any lookup. 401 means unknown; 403 means known and refused.

            CHECK · predict before you choose

            Which pair of assertions shows that a customer was refused before the database was touched?

            Explain it, then compare the reasoning

            Before looking: explain why your choice fits and why one other choice fails. This guide cannot grade your explanation. A correct choice is evidence of this check, not proof of complete understanding.

            • The response status is 403, and the body has an error message — A service that looked the user up first would send the same response.
            • getUser rejects with an AuthorizationError, and the spy on findUser was never called — The first assertion checks the answer and the second checks that no lookup happened.
            • The returned user has no hashedPassword, and its username is correct — Those describe a request that was allowed, not one that was refused.

            Source trail · Week 6 notes (Auth), captured October 13, 2026. The composed tests were run with Vitest 5.0 against the Week 6 starter.

            08

            Practice lab · trace, predict, write, and check

            Try it before revealing it.

            Choose a study session

            Time availableA useful sequence
            30 minutes10 min HTTP inputs/statuses → 10 min layers/ObjectId → 10 min answer recall questions without notes.
            90 minutes15 min Chapters 1–2 → 15 min middleware/HW1 → 20 min Mongo CRUD → 20 min REST trace → 20 min tests/practice.
            Several sessionsOne chapter per session. Explain the code aloud, do its lab, then mark confidence only when you can reproduce the idea.

            For each prompt, predict first, then reveal the explanation. If you want to run code, use a practice copy or your isolated test database and restore temporary edits afterward. The page itself is a study tool and never calls your APIs or changes course data.

            Lab 0 · JavaScript before the server

            J1. An order arrives with quantity as the string 2. What do order.quantity + 1 and order.size give?
            JavaScript
            const order = { pizza: 'cheese', quantity: '2' };
            console.log(order.quantity + 1);
            console.log(Number(order.quantity) + 1);
            console.log(order.size);
            Shell · output
            $ node lab-j1.js
            21
            3
            undefined

            The quantity is a string, so + joins text and gives 21. Number() converts it first, and then + adds. size was never set, so it is undefined, not an error.

            J2. Write isCheap(product), true when the price is under 5. Use it to print the names of the cheap products.
            JavaScript
            const products = [
              { name: 'Notebook', price: 3 },
              { name: 'Pencil', price: 1 },
              { name: 'Stapler', price: 9 }
            ];
            
            const isCheap = (product) => product.price < 5;
            
            const cheap = products.filter(isCheap);
            console.log(cheap.length);
            cheap.forEach((product) => {
              console.log(product.name);
            });
            Shell · output
            $ node lab-j2.js
            2
            Notebook
            Pencil

            isCheap is a callback. filter calls it once per product and keeps the products where it returns true. forEach then calls its own callback once per kept product.

            J3. Say this line in plain words: app.get('/menu', (req, res) => { res.send('menu'); });

            When a GET request for /menu arrives, run this function. app.get only stores the function. Express calls it later, once per matching request, with the request as req and the response as res. res.send sends the reply.

            J4. const product = findProduct(2); then console.log(product.price); prints undefined. Why, and what is the fix?

            findProduct returns a Promise, and a Promise has no price field. Add await, inside an async function or at the top of a module. The fixed version uses the stand-in db.js from the JavaScript section.

            JavaScript
            import { findProduct } from './db.js';
            
            const showPrice = async (id) => {
              const product = await findProduct(id);
              console.log(product.price);
            };
            
            await showPrice(2);
            Shell · output
            $ node lab-j4.js
            9

            Lab A · HTTP and TypeScript

            1. Label every input: POST /tracks?preview=true with JSON title and an Authorization header.

            Method POST; path /tracks; req.query.preview is query input; req.body.title is parsed body input; req.headers.authorization is header input. There is no path parameter in this particular route.

            2. Write /browse/:category and a query filter, then explain /browse/frozen?keywords=organic+corn.
            JavaScript
            app.get('/browse/:category', (req, res) => {
              res.send(`Searching for ${req.query.keywords} in the ${req.params.category} category`);
            });

            category is frozen; keywords decodes to organic corn. This is one route pattern serving many categories.

            3. Why must a ZIP code be a string? Why do numeric year filters need conversion?

            A ZIP is an identifier that may begin with zero. A book year is a number in the model, while a query arrives as text; strict equality requires converting the single-string query to a number before comparison.

            4. Recreate all six TypeScript exercise features from memory.

            Book interface; catalog: Book[]; BookStatus enum; typed parameters/returns; BookFilter = string | number with typeof narrowing; generic getFirstItem<Item> returning Item | undefined. Compare your runtime demo with the unchanged JavaScript output.

            5. What does getFirstItem([]) return? Can const catalog still receive a push?

            undefined for an empty array. Yes: const prevents reassigning catalog, not mutating the array. A precise annotation or nonempty input supplies the element type for inference.

            6. Convert "lcd,smart,sony" into the Week 1 display text.
            JavaScript
            const terms = 'lcd,smart,sony'.split(',').join(' AND ');
            // lcd AND smart AND sony

            Lab B · middleware and HW1

            7. Predict Week 2 POST /books with wrong auth and blank title; then HW1 POST /orders with no auth and {}.

            403 for Week 2, 401 for HW1. Both stop at route auth before field validation. If the JSON text itself is malformed, the parser stops earlier.

            8. Does parseBearerToken("Bearer nope") return null?

            No. It returns "nope" because the format is valid. Role middleware then rejects the candidate. Parsing and authorization answer different questions.

            9. Build an invalid order with at least three errors. How should the server respond?

            Use a blank customerName, unknown menuItemId, and quantity 0, with the customer token. Expect 400 and an errors array containing the safe-to-find problems. Without that token, expect 401. Do not invent option errors by reading an absent menu item.

            10. Trace a request to a path nobody registered.

            The parser runs where applicable, routers do not match, then the ordinary final fallback sends 404. No exception is necessary. The four-argument error handler is for propagated errors.

            11. Create an order, restart Node, then list orders. Predict the result.

            The array is process-local, so the newly started app has no saved orders. With staff auth, GET returns 200 and []. Mongo-backed storage in later exercises solves persistence across app restarts.

            Lab C · all four direct Mongo tasks and API checks

            12. Write findPricierTest and predict the original tape matches.
            TypeScript
            return await db.collection('products').find({ price: { $gte: 5 } }).toArray();

            With original prices 5.99, 3.99, and 2.01, only Duct Tape matches. $gte includes the exact boundary value 5.

            13. Write the Scotch Tape price update; what should not change?
            TypeScript
            return await db.collection('products').updateOne(
              { name: 'Scotch Tape' },
              { $set: { price: 4.29 } }
            );

            The name, quantity, and id stay unchanged. Check the matched/modified result and read the document afterward.

            14. Write deleteByNameTest and markOnSaleTest.
            TypeScript
            await db.collection('products').deleteOne({ name: 'Masking Tape' });
            
            await db.collection('products').updateMany(
              { price: { $lt: 5 } },
              { $set: { onSale: true } }
            );

            On the original fixture, both Scotch and Masking Tape are below 5. If you already deleted Masking Tape, only Scotch remains to match. Your prediction must reflect the current data state.

            15. Test PATCH: what proves it is partial?

            Create a tape, PATCH only price, GET it again, then assert the new price and unchanged name/quantity/id. Also assert 404 for a well-formed absent id and 400 for a malformed id. matchedCount, not modifiedCount, decides whether the product existed.

            16. Write a DELETE test that checks more than the status.

            Create or seed a product, delete it, check the week-specific success status, then GET it (404) and list products to prove it is absent. Delete an absent id for 404 and malformed id for 400.

            Lab D · Week 4 route and testing activities

            17. Add GET /products/:id to every required layer. Which layer does not need a new converter?

            Database adds lookup by ObjectId; service returns Product | null; controller reads params and chooses 200/404; route maps GET /:id. The existing productFromDocument model converter already handles the document.

            18. Write the two tests required by the “Write the tests” anchor.

            With the two seeded products, GET /products to obtain an existing id, then GET that id: 200 and toEqual the expected product object. For an absent well-formed id, GET it and assert 404. Both get the beforeEach fixture automatically. See the complete worked example in Chapter 7.

            19. Design customer/order routes without exposing the collection arrangement.

            GET/POST /customers/:id/orders expresses orders for a customer even when orders are stored in their own collection. Public paths model client needs; they do not have to mirror database nesting.

            20. Break one expected status in a test, run it, and read the failure. What should you restore?

            Use a practice copy or temporary edit, change the expectation to an intentionally wrong code, run the targeted test, and read expected/received output. Restore the correct assertion and confirm green. The aim is understanding failures, not weakening assertions.

            21. Bug hunt: add() inserts productInfo, then calls productFromFields(). You post a product with no modelNumber or quantity and get 201 with an id. What does Compass show, and what are the two fixes?

            Compass shows _id and only the fields you sent. The defaults were applied after the insert, to a value the controller discards except for its id. Fix one: call productFromFields() first and insert its result. Now every field is stored, but so is the placeholder id, beside _id. Fix two: leave id out of what you insert, by listing the six fields or with const { id, ...productWithoutId } = product, and return insertedId.toString(). Read the full bug story in Chapter 6.

            Source trail · Practice adapted from the linked NYT tasks and the six existing guides.

            09

            Active recall · 42 questions with answer reveals

            Close the notes. Explain it.

            Review a few cards each day

            Review mode shows the questions below one at a time and brings each one back after a growing gap: sooner when you miss it, later when you know it. Say your answer aloud or in your head, show the answer, then mark how it went. Nothing is graded, and nothing leaves this page.

            Review mode needs JavaScript. Without it, use the list below: say your answer, then open the card.

            Where the cards are

              How the schedule works
              • New cards come in the order of the list below, at most 10 on any one day. When you want more, ask for them.
              • Got it moves a card up one box, and a new card goes into box 1. Not yet sends a card back to box 1 and shows it again before the session ends.
              • A card comes back 1, 2, 4, 7 or 14 days after you last answered it, for boxes 1 to 5. A card in box 5 stays there for as long as you keep getting it.
              • A day here is a calendar day on this device, not 24 hours. A card answered late at night is ready the next morning.
              • An extra practice round changes nothing in the schedule.
              • The schedule is kept only in this browser, apart from your check results, and is the same in all three languages. If the wording of a question is edited, only that card starts again.
              • Your answer is never checked. Only you decide between Got it and Not yet.

              Answer aloud before opening each card

              These questions cover the complete core path. A confident explanation should include an example, not just a remembered word. Use “Show all answers” above when reviewing or printing.

              1. What makes an API route unique?

              The HTTP method plus its path pattern. GET /orders and POST /orders are different routes.

              2. Which input source uses req.params, req.query, and req.body?

              Path placeholders, URL query pairs, and parsed submitted content respectively.

              3. How do HTML form inputs become object keys?

              Their name attributes choose submitted keys; action and method choose the endpoint.

              4. Why put body parsers first?

              Later middleware/handlers need parsed req.body; malformed data may fail in the parser first.

              5. What does return after res.status(...).send(...) do?

              Stop the current handler from continuing and attempting a second response.

              6. What does a union type mean?

              A value may be one of the named alternatives; narrow it with a runtime check before using type-specific operations.

              7. Interface versus runtime validation?

              An interface is erased compile-time information. Validation actually inspects incoming runtime values.

              8. Why return Book | undefined from a lookup?

              The array may contain no matching book.

              9. What does a generic preserve?

              The relationship between the input’s element type and the output type across different calls.

              10. What do Partial and Omit do?

              Partial makes fields optional; Omit excludes named fields from a compile-time type.

              11. What does await do?

              Wait within the async function for the Promise’s settlement. Rejection can propagate as an error.

              12. Which nullish values activate ??, and does zero activate it?

              Null and undefined activate it. Zero, false, and an empty string do not.

              13. Router mount /menu plus internal /:id equals what?

              Public /menu/:id.

              14. Two ordinary middleware outcomes?

              Call next() or end the response. next(error) forwards an error.

              15. What identifies error middleware?

              The four-parameter shape (err, req, res, next), registered after relevant routes.

              16. Why does middleware order change a status?

              An earlier middleware can finish the request before a later validator or handler runs.

              17. How do Week 2 and HW1 auth failures differ?

              The classroom Week 2 implementation uses 403; HW1 requires 401, including wrong-role tokens.

              18. How does a valid bearer format differ from a valid credential?

              Format lets the parser extract a candidate. Role middleware still has to accept that candidate.

              19. Why collect all order errors?

              The client can fix multiple fields together, while structural guards prevent unsafe checks.

              20. What should an unknown collection filter return?

              Usually the exercise’s 200 with []; it is different from an unknown single resource, which is 404.

              21. What is lost when an in-memory server restarts?

              Process-local state such as the catalog/order array and counter.

              22. Collection/document/field corresponds roughly to what?

              Table/row/column, without implying identical relational behavior.

              23. Does JSON contain only strings?

              No: it has strings, numbers, booleans, null, arrays, and objects. BSON adds richer database types.

              24. What does find return, versus findOne?

              find returns a cursor. findOne returns a document or null.

              25. What do $gte, $lt, and $set mean?

              At least, strictly less than, and change/add only the supplied fields.

              26. Why can modifiedCount be zero on a valid update?

              The document already contains the requested value; matchedCount still shows it existed.

              27. Why convert a URL string to ObjectId?

              The query must use the BSON type of the stored generated _id.

              28. Malformed id versus absent well-formed id?

              400 for unusable format; 404 when the valid query finds no resource.

              29. Why have connect/init parameters?

              The app and tests can use different URIs/databases without changing database-layer code.

              30. What makes a request stateless?

              It provides the context needed to interpret it, rather than depending on a prior request’s conversational state.

              31. What is a resource representation?

              A client-visible copy of resource state; changing the copy does not directly edit the server’s stored resource.

              32. Name REST’s six constraints.

              Uniform interface, client–server, statelessness, cacheability, layered system, optional code on demand.

              33. Where should HTTP statuses be chosen in Week 4?

              Controllers. Services/database return application data or absence.

              34. Where does _id become id?

              productFromDocument in the model layer converts ObjectId to a public string.

              35. What is special about a 204 response?

              Successful completion with no response body.

              36. Why does temporary Mongo not eliminate beforeEach?

              It is fresh between runs, but tests within one run can still share and mutate its data.

              37. Why seed GET tests directly through the database?

              So their setup does not depend on a working POST route.

              38. toBe versus toEqual?

              Same primitive/object identity versus equal nested contents. Use toEqual for separately created products.

              39. Why pair negative and positive assertions?

              An empty wrong result can satisfy “not containing X”; positive assertions prove the expected result exists.

              40. What is arrange–act–assert?

              Prepare known data, execute the operation, then check the observable result and state.

              41. Why assert generated values by shape/properties?

              Ids and timestamps vary. Test the required format/parseability rather than one exact run’s value.

              42. What startup behavior does Supertest miss?

              index.ts listener/database initialization and the normal deployment/runtime configuration.

              Source trail · Consolidated self-checks from every topic.

              10

              Commands, ports, status codes, troubleshooting, and vocabulary

              The desk reference.

              Run the right script in the right folder

              Open a terminal in the specific project folder. A script only exists when that repository’s package.json defines it. These commands are a reference; the study page does not execute them.

              ProjectScripts to knowNormal app / database
              Classes/week1hellonpm startPort 1679; no database.
              Classes/si-679-f-26-week1-nyt-kelvintigernpm startPort 3000; no database. Its npm test is a placeholder, not a local test suite.
              Classes/si-679-f-26-ts-basics-kelvintigertypecheck, build, start, dev, jsConsole catalog; no HTTP server; no npm test script.
              Classes/si-679-f-26-week02-nyt-kelvintigertest, test:watch, typecheck, build, dev, startPort 6790; in-memory catalog.
              Homework/si-679-f-26-hw1-kelvintigertest, test:watch, typecheck, build, devPort 3000; in-memory orders. No npm start script.
              Classes/si-679-f-26-week03-nyt-kelvintigertest, test:watch, typecheck, build, explore, dev, startPort 6790; app week3app; exploration week3db.
              Classes/si-679-f-26-week-04-nyt-kelvintigertest, test:watch, typecheck, build, dev, startPort 6790; week4/products; sampleData/products.json.
              CommandPurpose
              npm ciInstall the lockfile’s dependency set for an existing checkout with a valid lockfile.
              npm installInstall/update dependencies; used by the lecture starters.
              npm testUsually vitest run, once, in the repositories defining it.
              npm run test:watchRerun tests as source changes.
              npm run typechecktsc --noEmit checks types without generating files.
              npm run buildCompile TypeScript into dist. HW1 uses a build config excluding tests.
              npm run devRun TypeScript with tsx (usually watch); does not replace a typecheck.
              npm startRuns whatever start means in that project: watched JS or built dist.
              Shell
              # From the existing Week 4 repository:
              npm ci
              npm test
              npm run typecheck
              npm run build
              
              # For manual practice, separately start/connect your normal MongoDB,
              # then import sampleData/products.json into week4/products in Compass.
              npm run dev
              # Keep this terminal open; use a second one for curl. Stop with Ctrl-C.

              Default Mongo URI is mongodb://127.0.0.1:27017. A manually started server can use mongod --dbpath ~/data/mdata if that is the data directory you created. These are course setup instructions, not a claim that mongod/mongosh/Compass are currently installed or running on this Mac.

              A complete Week 4 manual round trip

              Shell · delete only the practice product you created
              curl -i http://localhost:6790/products
              
              curl -i -X POST http://localhost:6790/products   -H 'Content-Type: application/json'   -d '{"modelName":"Practice coffee maker","price":25,"quantity":3}'
              
              # Replace YOUR_ID with the id just returned above.
              curl -i http://localhost:6790/products/YOUR_ID
              curl -i -X DELETE http://localhost:6790/products/YOUR_ID
              curl -i http://localhost:6790/products/YOUR_ID

              Expect list 200, create 201, read 200, delete 204 with empty body, then read 404. -i shows response headers/status; -X chooses the method; -H adds headers; -d supplies the body. Use your own new practice record for deletion.

              Shell · supplied classroom credentials
              # HW1 manual calls (server on port 3000):
              curl 'http://localhost:3000/menu?category=pizza'
              curl -X POST http://localhost:3000/orders   -H 'Authorization: Bearer slicedrop-customer-secret'   -H 'Content-Type: application/json'   -d '{"customerName":"Ada","items":[{"menuItemId":"soda","quantity":1}]}'
              curl http://localhost:3000/orders   -H 'Authorization: Bearer slicedrop-staff-secret'

              Keep HW1’s POST and list in the same running process to see the order. Quote URLs containing & in your shell, and replace illustrative ids instead of copying them blindly.

              Statuses and contracts you should not blur together

              CodeMeaningCourse example
              200Successful responseGET/list; Week 1 tracks; Week 3 PATCH/DELETE.
              201Created a resourceWeek 2 books, HW1 orders, Weeks 3–4 products POST.
              204Success with no bodyLocal Week 4 DELETE.
              400Unusable client inputBad JSON/fields or malformed id.
              401Authentication credential not acceptedHW1 missing/bad/wrong-role token. General semantics differ from the exercise’s simplified role handling.
              403ForbiddenGeneral lack of permission; Week 2 uses it for all failed classroom token checks.
              404No requested route/resourceWell-formed absent id; unknown route. Empty filtered collections remain 200.
              500Unexpected server errorThrown application error; inspect server logs.
              DistinctionEarlier exampleLater / other example
              IdWeek 1 menu-style text / Week 2 numeric Date.nowMongo ObjectId represented as hex text in URLs.
              PATCH pathWeek 2 /books, body contains idWeek 3 /products/:id. Week 4 local exercise has no PATCH route.
              DELETE pathWeek 2 /books, body idWeeks 3–4 /products/:id.
              DELETE statusWeek 2/3 200Week 4 local 204; notes allow 200 or 204.
              StorageWeek 2/HW1 process-local arraysWeeks 3–4 persistent normal Mongo; temporary Mongo for tests.
              Response identityWeek 3 documents show _idWeek 4 public Product exposes id and omits _id.
              Product shapeWeek 3 name/price/quantityWeek 4 modelName/modelNumber/manufacturer/color/price/quantity/id.

              Debug from the outside inward

              SymptomLikely place to inspectNext check
              Connection refused immediatelyHTTP listener or wrong portIs the correct dev server running? Check 1679/3000/6790 and terminal errors.
              Long wait, then Mongo connection errorDatabase server / URIConfirm mongod reachable at the chosen URI, not just npm driver installed.
              Cannot POST /products / 404 HTMLMethod, mount, registered routeCheck POST vs GET and router relative path/prefix.
              Undefined body / empty defaultsContent-Type, body format, parserPostman raw JSON; express.json before route; inspect local validation response.
              GET list is []Chosen database/collection/fixtureweek3db vs week3app vs week4; did you import/seed into the same place?
              401 / 403 before data errorsAuth middlewareCorrect exact classroom credential/role; week-specific status.
              400 for YOUR_IDId shapeReplace the placeholder with a real returned 24-hex id.
              Headers already sentControl flowLook for continuing after a response or calling next after responding.
              Request hangsMiddleware/await/connectionDid every path call next or answer? Did the database operation finish?
              Test passes once but fails laterShared stateReset between tests; avoid persistent database leftovers; restore mocks.
              Type errors despite running dev serverCompiler vs runnertsx execution is separate from npm run typecheck.
              Port already in useAnother running appStop the conflicting process or use the project’s configurable port.

              Work in this order: method/full path → status → headers/body → parser/middleware → handler/service → database state → response. For a 500, read the log. For tests, read the failure’s expected/received values and fixture. Once fixed, rerun the relevant test and typecheck.

              Plain-language glossary

              Every term the lessons use, in alphabetical order. Each entry names the lesson that teaches it.

              In a lesson, a term with a dotted underline opens its meaning in place.

              TermMeaning
              APIThe agreement about how software asks other software to do things: which requests are allowed, what to send, and what the replies mean. Taught in 0.2 · Two kinds of asking
              ArgumentThe value passed to a function in a call. Taught in Functions: instructions with a name
              ArrayAn ordered list of values. Taught in Arrays: an ordered list
              Arrow functionA short way to write a function: the parameters, then an arrow, then the body. Taught in Functions: instructions with a name
              AssertionA test statement of expected behavior, checked by a matcher. Taught in Choose a matcher that asks the right question
              AuthenticationChecking who is making the request. Taught in 0.3 · Open the kitchen door
              AuthorizationChecking whether the requester may do this action. Taught in 0.3 · Open the kitchen door
              BackendThe part of a system that receives a request, checks it, does the work, and replies. Taught in 0.1 · Start with something you already know
              bcryptA password-hashing algorithm that mixes in a random salt and is slow on purpose. Taught in Never store the password
              BodyThe submitted contents of a request, or the returned contents of a response. Taught in 0.2 · Two kinds of asking
              BooleanA value that is either true or false. Taught in Values: text, numbers, and true or false
              BSONThe binary format MongoDB stores, with extra types such as ObjectId and dates. Taught in Choose a data model; learn the vocabulary
              CacheableA response says whether it may be saved and reused later instead of being requested again. Taught in REST is an architectural style
              CallbackA function given to another function to run at the appropriate point. Taught in Callbacks: hand a function to another function
              ClaimOne field in the payload of a token, such as sub, exp or role. Taught in A JWT: anyone can read it, only the server can make it
              ClientThe software that asks another program for something. Taught in 0.1 · Start with something you already know
              Code on demandAn optional REST constraint: a server may send code for the client to run. Taught in REST is an architectural style
              CollectionA named group of documents in a MongoDB database, roughly a table. Taught in Choose a data model; learn the vocabulary
              CommandOne instruction you type in a terminal and run by pressing Enter. Taught in Open a terminal and move around
              CommitOne saved version of a project in git, with a short message that says what changed. Taught in Save and hand in work with git and GitHub
              Compile timeWhen the types and source of a program are checked, before it runs. Taught in What TypeScript changes
              ContractThe agreed input/output behavior that clients and tests rely on. Taught in The exact Week 2 contract
              ControllerThe layer that handles the web conversation: it reads the request, asks a service to do the work, and builds the response. Taught in Give each layer one responsibility
              CRUDCreate, read, update, and delete: the four basic operations on stored data. Taught in CRUD uses query objects and update operators
              CursorAn object through which database query results are retrieved/iterated. Taught in CRUD uses query objects and update operators
              DatabaseA program that stores durable records, so data can be looked up later, even after the server restarts. Taught in 0.3 · Open the kitchen door
              DependencyA package needed by the app. Taught in Create a project and install Express
              DestructuringMaking names from the fields of an object in one statement. Taught in Three shortcuts the course uses everywhere
              Dev dependencyTooling used to develop, build, or test the app. Taught in TypeScript setup, headers, and all three NYT blocks
              DocumentOne stored record in MongoDB, like a filled-in form. Taught in Choose a data model; learn the vocabulary
              DriverThe npm package that lets Node code talk to MongoDB and translates between JavaScript values and BSON. Taught in Choose a data model; learn the vocabulary
              EndpointA client-accessible operation identified by method and path pattern. Taught in Start with the conversation
              EnumA TypeScript type that gives names to a fixed set of allowed values. Taught in What TypeScript changes
              Environment variableA named value that lives outside the code and that a program reads when it runs, such as JWT_SECRET. Taught in The secret lives in the environment
              Error handlerExpress middleware with four parameters (err, req, res, next) that answers when an error is passed along the chain. Taught in Keep startup separate; put errors last
              ExportA declaration that marks what a file offers to other files. Taught in Modules: one file uses another
              ExpressA library that helps a Node program receive HTTP requests, match routes, parse data, and send responses. Taught in Start with the conversation
              FalsyCounted as false when used as a condition. 0, an empty string, null and undefined are falsy. Taught in Decisions: if, else, and comparisons
              FieldOne named value in an object or a document. Taught in Objects: labeled facts
              FixtureKnown data arranged so a test has an unambiguous expected result. Taught in Start once; reset each time; stop once
              FrontendThe part you interact with: buttons, forms, and what the screen shows. Taught in 0.1 · Start with something you already know
              FunctionNamed reusable code that runs when it is called. Taught in Functions: instructions with a name
              GenericA function or type with a type parameter, so one definition keeps the type each caller uses. Taught in Preserve a type with a generic
              gitA program that records versions of a project folder. Taught in Save and hand in work with git and GitHub
              HandlerThe function Express runs when a matching request arrives. Taught in Callbacks: hand a function to another function
              HashA fixed-size fingerprint computed from an input, which cannot be turned back into that input. Taught in Never store the password
              HeaderA named line of extra information on a request or response, such as Content-Type. Taught in Start with the conversation
              HookA function the test runner calls around tests, such as beforeAll or beforeEach, to set up and clean up. Taught in Start once; reset each time; stop once
              HTTPThe agreed format for web requests and responses. Taught in 0.2 · Two kinds of asking
              ImportA declaration that brings names from another file into this one. Taught in Modules: one file uses another
              In-memoryKept only inside the running program, so it is lost when the process restarts. Taught in Setup and manual client practice
              IndexThe position of an item in an array, counted from 0. Taught in Arrays: an ordered list
              InterfaceA TypeScript description of the fields an object has and the type of each. Taught in What TypeScript changes
              JSONText with a defined syntax for structured data: strings, numbers, booleans, null, arrays, and objects. Taught in Know where the input lives
              JWTA signed token in three parts, header.payload.signature, that anyone can read and only the server can make. Taught in A JWT: anyone can read it, only the server can make it
              LayerOne code area that owns one responsibility with clear boundaries. Taught in Give each layer one responsibility
              Layered systemA system built in layers, where each part sees only the layer next to it. Taught in REST is an architectural style
              localhostA name that means your own computer. Taught in Run the server and visit it
              LockfileRecords resolved package versions to reproduce dependency installation. Taught in Create a project and install Express
              LoopCode that runs the same statements more than once. Taught in Loops: repeat for each item
              MatcherThe part of an assertion that says how to compare, such as toBe or toEqual. Taught in Choose a matcher that asks the right question
              MethodThe word in a request that names the kind of action, such as GET or POST. Taught in 0.2 · Two kinds of asking
              MiddlewareA function in the request chain that can continue, respond, or forward an error. Taught in Middleware either continues or answers
              MockA test replacement for behavior, such as deliberately throwing an error. Taught in Start once; reset each time; stop once
              ModelCode that describes the shape of the data the program works with; in Week 4 it also turned stored documents into that shape. Taught in Give each layer one responsibility
              ModuleA file of code that other files can use. Taught in Modules: one file uses another
              MongoDBA database that stores documents in collections. Taught in Choose a data model; learn the vocabulary
              MongoMemoryServerA real, temporary MongoDB process that tests start and stop. Taught in Three tools, three jobs
              MountTo attach a router to the app under a path prefix. Taught in Organize routes without changing the API
              NarrowingChecking which type a union value really has, for example with typeof, so TypeScript knows it inside that branch. Taught in Narrow a union before using it
              NodeA program that runs JavaScript outside the browser. Taught in Install Node and check it
              npmThe tool installed with Node that downloads packages and runs the scripts a project defines. Taught in Install Node and check it
              ObjectA group of labeled values. Taught in Objects: labeled facts
              ObjectIdThe id type MongoDB generates: a 12-byte value usually shown as 24 hexadecimal characters. Taught in An id’s shape and its existence are separate questions
              Optional chainingReading a property with ?. so that a null or undefined value gives undefined instead of a crash. Taught in Narrow a union before using it
              PackageA piece of code other people wrote that npm downloads into your project. Taught in Install Node and check it
              ParameterA name for one of the inputs of a function. Taught in Functions: instructions with a name
              ParsingTurning text into a value the program can use, as express.json() does with a JSON body. Taught in Know where the input lives
              PathThe part of a request that says what the action is about, such as /tracks. Taught in 0.2 · Two kinds of asking
              Path parameterA placeholder in a route path, such as :id, whose real value arrives in req.params. Taught in Know where the input lives
              PersistenceStored state survives the application process restarting. Taught in 0.3 · Open the kitchen door
              PortA number that chooses which listening program on a computer receives the request. Taught in Run the server and visit it
              PostmanAn app that sends an HTTP request you build by hand and shows the whole response. Taught in Send requests with Postman
              PromiseAn object that stands for a result that is not ready yet. await gets its value. Taught in Waiting for slow work: Promise, async, await
              PropertyOne label with its value in an object, also called a field. Taught in Objects: labeled facts
              Query objectAn object that tells MongoDB which documents an operation should match. Taught in CRUD uses query objects and update operators
              Query parameterA named value after the ? in a URL, read from req.query. Taught in Know where the input lives
              Regular expressionA pattern that describes which text matches, written between slashes in JavaScript. Taught in Parsing a credential is different from accepting it
              RepositoryA folder that git tracks. Taught in Save and hand in work with git and GitHub
              Repository layerThe layer that reads and writes the database and turns stored documents into the application’s own objects, such as a Product. Taught in Four layers, four jobs
              RepresentationThe client-visible copy of a resource’s state. Taught in REST is an architectural style
              RequestOne message a client sends to a server to ask for something. Taught in 0.1 · Start with something you already know
              ResourceA thing the API lets clients work with, such as an order or a product. Taught in 0.2 · Two kinds of asking
              ResponseThe reply a server sends back to one request. Taught in 0.1 · Start with something you already know
              RESTA set of consistent conventions for APIs organized around resources, built on the HTTP model of the web. Taught in REST is an architectural style
              Role-based access controlDeciding what a caller may do from the role they hold, such as customer or staff. Taught in Roles: requireRole runs after requireJwt
              RouteCode that matches the method and path of a request to the function that handles it. Taught in Read a route from left to right
              RouterAn Express object that collects related routes so the app can mount them under one path prefix. Taught in Organize routes without changing the API
              RuntimeWhen the program executes. Taught in What TypeScript changes
              SaltA random value mixed into a password before hashing, so equal passwords get different hashes. Taught in Never store the password
              Seed dataKnown starting records inserted before exploring or testing. Taught in Start once; reset each time; stop once
              Separation of concernsGiving each code area one responsibility with clear boundaries. Taught in Give each layer one responsibility
              SerializationConverting a value into transferable text/binary form, such as JSON. Taught in Read a route from left to right
              ServerThe program that receives requests, does the work, and sends responses. Taught in 0.1 · Start with something you already know
              ServiceThe layer that carries out the work and rules of the application. It knows nothing about req or res. Taught in Give each layer one responsibility
              SpyA test double that wraps a real function and records how it was called; on its own it still runs the real code. Taught in Fake, mock, spy: control and observation
              Stack traceThe chain of calls that led to an error, newest first. Taught in When code fails: throw, try, catch
              StatelessEach request supplies the context needed to understand it; the server does not depend on remembered conversation. Taught in REST is an architectural style
              StatementOne complete instruction in a program. Taught in Run one line of JavaScript
              Status codeThe number in a response that says what happened, such as 200, 201, 400 or 404. Taught in Start with the conversation
              StringA value that is text, written between quotes. Taught in Values: text, numbers, and true or false
              SupertestA library that lets a test send HTTP requests to an Express app without starting a server by hand. Taught in Three tools, three jobs
              Template literalA string between backticks that can hold values. Taught in Three shortcuts the course uses everywhere
              TerminalA window where you type a command, press Enter, and read the text the computer prints back. Taught in Open a terminal and move around
              TestCode that sends known inputs and checks for expected results. Taught in 0.3 · Open the kitchen door
              Test doubleA stand-in, controlled by the test, for something the code under test calls, such as a mock or a spy. Taught in Fake, mock, spy: control and observation
              Test isolationA test’s result does not depend on leftovers from another test/run. Taught in Start once; reset each time; stop once
              TokenA secret string a client sends, here in the Authorization header, so the server can decide whether to accept the request. Taught in Four endpoints; two different roles
              TruthyCounted as true when used as a condition. Taught in Decisions: if, else, and comparisons
              TypeThe kind of data a value is, such as string, number or boolean. Taught in Values: text, numbers, and true or false
              Type annotationA TypeScript note after a name, such as : string, that says which type the value must be. Taught in What TypeScript changes
              Type parameterA placeholder for a type, chosen or inferred for each call. Taught in Preserve a type with a generic
              TypeScriptJavaScript with type annotations that a compiler checks before the program runs. Taught in What TypeScript changes
              Uniform interfaceOne consistent way for clients and servers to interact: standard HTTP methods used in their standard meaning and consistent endpoint names. Taught in REST is an architectural style
              UnionA type that allows one of several possibilities, such as string | number. Taught in Narrow a union before using it
              URLA web address: protocol, host, optional port, path, and optional query. Taught in Start with the conversation
              ValidationChecking that incoming data is usable before acting on it. Taught in Read a route from left to right
              ValueOne piece of data. Taught in Values: text, numbers, and true or false
              VariableA name that holds a value, made with const or let. Taught in Names for values: const and let
              VitestThe test runner the course uses: it runs tests, hooks, matchers, and mocks. Taught in Three tools, three jobs
              Watch modeReruns/restarts work when watched files change. Taught in Setup and manual client practice

              Source trail · Scripts and source in the current seven local projects, plus course notes.

              11

              Coverage, original guides, and source library

              Every topic has a trail.

              What was reviewed

              Prepared September 29, 2026, from all seven local coursework folders, their available guides, source files/tests, and all four supplied course pages. The local Homework folder contains HW1; no local HW2/HW3 was present. This is a consolidated technical study guide for Weeks 1–6, the TypeScript basics activity, and HW1, rather than a live Canvas assignment/deadline audit.

              The two Confluence pages were read in a rendered browser and saved as detailed topic/exercise extracts. The two public GitHub notes were downloaded and their full main text retained. All four were readable. Administrative announcements and historical due dates remain in the source extracts but are not presented as current deadlines. Source examples with obvious syntax/wording mistakes are clarified rather than copied blindly.

              Local code is used when describing your repository’s exact behavior. Lecture-only designs and local refinements are labeled. This guide creation did not change, submit, or rerun coursework. Earlier stored grading/install claims are not used as current proof.

              The public Week 3 prep page was added on October 4, 2026 as the source for the MongoDB setup topic. Its HTML and main text are retained with the other snapshots. The setup section’s terminal, editor and git basics are general usage, not course text. Its example output was captured on one Mac and is labeled as an example.

              The Week 4 post-class memo was added on October 4, 2026 as the source for the bug-story topic in Section 6 and for the revised Now You Try #2. Its HTML and main text are retained with the other snapshots.

              The JavaScript section was added on October 4, 2026. It is general JavaScript usage, not course text, and it reuses this guide’s pizza, book and product examples. Every example in it and every output shown was run with Node 24 on one Mac. In error output, this machine’s folder was changed to an example folder.

              The public Week 5 notes were added on October 6, 2026 as the source for chapter 7+. Their HTML and main text are retained with the other snapshots. That chapter also follows the Vitest guide to mock functions, which is linked, not retained. Its arrange, act, assert Python example was run with pytest 9.1; its other three Python examples were run with Python 3.9. Its composed unit-test examples were run with Vitest 3.2 against a copy of the notes’ service with the repository stubbed, then with its composed end-to-end tests under Vitest 5.0 against the Week 5 starter.

              The public Week 6 notes were added on October 13, 2026 as the source for chapter W6. Their HTML and main text are retained with the other snapshots. Code labelled as from the Week 6 notes is verbatim. The composed end-to-end and unit tests were run with Vitest 5.0 against the Week 6 starter with the lecture code in place.

              Chapter 2+ quotes the class exercise repos for Weeks 2, 3, 4 and 6, which the course posts publicly, from snapshots pinned in the manifest. Chapter HW2 summarizes and links the public HW2 instructions and quotes class code from the Week 4 and Week 6 repos the same way. Neither chapter contains homework code, and a check fails if a homework solution name appears in either.

              Course-source coverage checklist

              Source sectionCovered materialStudy here
              Week 1 · Getting started / first route / routesNode/npm/ESM/watch, Express app/listen, method+path, response APIsOpen topic →
              Week 1 · POST / forms / form middlewareaction/method/name, req.body, URL-encoded parser, ZIP textOpen topic →
              Week 1 · Query strings / route params / errorsreq.query/params, decoding, numeric guards, 400/default 200Open topic →
              Week 1 · NYT #1catalog, play, getform/submitform, browseOpen topic →
              Week 1 · Postman / JSON / NYT #2postData, hithere, tracks/error/timestamp, products AND keywordsOpen topic →
              TypeScript basics · all six tasksInterface, annotation, enum, signatures, union/narrowing, genericOpen topic →
              Class exercise repos, Weeks 2–6Delete the types: colons, angle brackets, type lines, as constOpen topic →
              Class exercise repos, Weeks 2–6The twelve shapes, by how often they appear in the course’s .ts filesOpen topic →
              Class exercise repos, Weeks 2–6Controller anatomy, res.status().json(), .js imports, describe/it/expect, SupertestOpen topic →
              tsc on a copy of a class repoNine real TypeScript errors and how to read each partOpen topic →
              Vitest on a copy of a class repoOne real test failure, line by lineOpen topic →
              Class exercise repos · reviewThe blank-file drillOpen topic →
              Week 2 · Setting up TS / routing / NYT #1NodeNext/build/dev/typecheck, Router/service, combined exact filtersOpen topic →
              Week 2 · HTTP headers / middlewareHeader metadata/case/custom values, logger, auth, route orderOpen topic →
              Week 2 · Errors / statuses / NYT #2Four-argument handler, badroute, 201, GET by id, PATCH body idOpen topic →
              Week 2 · Supertest / app split / state leaks / NYT #3Request chains, reset, body/text assertions, auth/filter/delete casesOpen topic →
              HW1 · complete local implementation and guideMenu/filter, two roles, exact parser, all order validation, state/errors/testsOpen topic →
              Week 3 · What is Mongo / NoSQL / BSONSQL tradeoffs, collection/document/field, nesting/types, server vs driverOpen topic →
              Week 3 · Database / starter / connectionCompass/mongosh, URI, client/db/collection/ping/disconnect, watch duplicatesOpen topic →
              Week 3 · Inserts / finds / queries / updates / deletesinsertOne/Many, cursor/findOne, ObjectId, comparisons, $set/results/optionsOpen topic →
              Week 3 · NYT #1 + stretchPricier products, Scotch price, Masking deletion, onSale many updateOpen topic →
              Week 3 · Express / DB / router / app / server / PostmanAll CRUD routes, connection before listen, type shapes and statusesOpen topic →
              Week 3 · Live DB tests / temporary Mongo / matchersCross-run and cross-test contamination, hooks, matchers/rejectionsOpen topic →
              Week 3 · NYT #2 + stretchPATCH preserved fields/404, DELETE 200/404, malformed-id middleware testsOpen topic →
              Week 4 · REST / principles / scenarioSix constraints, representation, entities/fields/endpoints/policy boundariesOpen topic →
              Week 4 · Architecture / build GET / all layersLayer responsibilities, imports, converters, map, glue/errors/startupOpen topic →
              Week 4 · Build POST / DB/model/service/controller/router / PostmanPartial, ??, insertedId, 201/id, readback/Compass, troubleshootingOpen topic →
              Week 4 · NYT #1 + DELETE stretchGET single/404/public id, delete count/readback, controller HTTP decisionOpen topic →
              Week 4 · Supertest / temporary DB / init / hooks / seed / assertionsApp vs startup boundary, direct DB fixtures, matcher examplesOpen topic →
              Week 4 · NYT #2 / Write the tests / DELETE stretch testsExact requested-object/404 tests; successful/absent deletion and list stateOpen topic →
              Week 1 · Create an Express project / Postman setup / Classroom50 flownpm init, express@latest, type module, start script, localhost:3000, accept/clone/pushOpen topic →
              Week 3 prep · Install / start / verify MongoDBCommunity Edition, mongosh, Compass, service or --dbpath, port 27017, admin/config/localOpen topic →
              Week 4 · Post-class memoadd() line order, defaults before insert, placeholder id beside _id, destructuring with rest, id-only return, revised NYT #2Open topic →
              Week 5 · The app you’ve been given / Following one requestFolder roles, repository translation, app vs indexOpen topic →
              Week 5 · Router / controller / service / repository / modelPATCH path, ??, find then validate then write, findOneAndUpdate, layer guaranteesOpen topic →
              Week 5 · A brief digression into error handlingValidationError, error handler 400/500, null as 404, absent vs malformed idOpen topic →
              Week 5 · Unit testing / regression / integrationUnit, integration, end-to-end and regression; fakes, mocks and spies namedOpen topic →
              Week 5 · Before anything else / Streamlining test runningnpm install/test, test:unit and test:e2e filters, --passWithNoTests, it.todoOpen topic →
              Week 5 · E2E testing / Test #1 to #3Arrange, act, assert; a new value unlike the seed; matchersOpen topic →
              Week 5 · How the suite is set upbeforeAll, beforeEach reset, afterAllOpen topic →
              Week 5 · Test #2, Test #3, Now You Try #1Negative quantity, 404 for an absent id, the three todo testsOpen topic →
              Week 5 · Mocks and spiesFake, mock, spy; control and observationOpen topic →
              Vitest guide · Mock functionsvi.fn, call matchers, mockReturnValue, once queue, mockImplementationOpen topic →
              Vitest guide · Mock functionsmockResolvedValue, mockRejectedValue, rejects.toThrowOpen topic →
              Vitest guide · Mock functionsmock.calls, mock.results, Nth and last call, argument referencesOpen topic →
              Week 5 · Mocks and spies / Our first real unit testvi.spyOn alone and with an answer, mocking the dependencyOpen topic →
              Week 5 · Setting up the file / Pin the boundaryImports, restoreAllMocks, makeProduct, low stock and its boundaryOpen topic →
              Week 5 · Asking the spy what it saw / A spy with no mock / Now You Try #2Exact write arguments, not.toHaveBeenCalled, price and missing-product testsOpen topic →
              Week 5 · Now You Try #2, task 3createProduct defaults, unit vs end-to-end evidenceOpen topic →
              Vitest guide · Mock functionsmockClear, mockReset, mockRestore, restoreAllMocks, restoreMocksOpen topic →
              Vitest guide · Mock functionsvi.mock, vi.mocked, hoistingOpen topic →
              Week 5 notes and Vitest guide · reviewMisleading results and seven questions to judge a testOpen topic →
              Week 5 notes and Vitest guide · reviewFifteen practice questions and five tasksOpen topic →
              HW2 instructions · Overview / Your repo does not compileMemory to MongoDB, one folder per layer, async validation, the two typecheck errorsOpen topic →
              HW2 instructions · What you build / Endpoints / Status code summaryEach task’s layer, its lesson and its status codesOpen topic →
              Class exercise repos, Weeks 4 and 6Repository, 409, 204, guard order, a req.query filter, Express 5 error forwardingOpen topic →
              HW2 instructions · Tips / GradingTypecheck first, one layer and one suite at a time, reading failures, the 120 pointsOpen topic →
              Class code and this guide · reviewSix questions an instructor might ask, answered from class codeOpen topic →
              Week 6 · Auth and AuthAuthentication and authorization, shared secrets in HW1 and HW2, the five routesOpen topic →
              Week 6 · Passwords, briefly / Hashing / bcryptOne-way hash, salt, cost, the parts of a bcrypt string, HTTPSOpen topic →
              Week 6 · What you’ve been givenUser, NewUser and PublicUser; the users repositoryOpen topic →
              Week 6 · Registering a userregisterUser, ConflictError and 409, the error handler, controller and routeOpen topic →
              Week 6 · Logging inbcrypt.compare, one message for both failures, why a token is neededOpen topic →
              Week 6 · Tokens and JWTs / What’s in a JWTHeader, payload, signature, base64url, claims, jwt.ioOpen topic →
              Week 6 · Where the secret lives / Signing a token at login.env and .env.example, --env-file, config.ts, the test secret, restarting the dev server, dotenvOpen topic →
              Week 6 · Checking the token: requireJwt middleware / GET /auth/meBearer header, jwt.verify, res.locals.user, 401Open topic →
              Week 6 · Writing a test together / Now You Try #1Tokens signed in the test: missing, not a JWT, wrong secret, expired, edited, real loginOpen topic →
              Week 6 · Roles in the token / Only staff: requireRole / Testing with npm testSeeded staff user, role claim, middleware factory, guard order, 401 and 403 and 201, staff and customer test tokensOpen topic →
              Week 6 · Ownership: what roles can’t say / Testing ownershipgetUser in the service, why not middleware, the caller, GET /users/:id testsOpen topic →
              Week 6 · A unit test for the service / How the unit test earns its place / Now You Try #2Spy on findUser, check before lookup, the id leak, four more unit testsOpen topic →
              Week 6 · Wrapping upHTTPS, revocation, stale roles, refresh, reset, auth providersOpen topic →
              Week 6 notes · reviewTwelve practice questions and three tasksOpen topic →

              The course pages this guide covers

              Week 1 · Course Intro, Express Intro ↗

              Rendered-page topic/exercise extract

              Week 2 · Express Routing & Middleware, Unit Testing ↗

              Rendered-page topic/exercise extract

              Week 3 · MongoDB ↗

              Full main-page text snapshot

              Week 4 · REST APIs + Write the tests ↗

              Full main-page text snapshot

              Week 3 prep · Install MongoDB ↗

              Full main-page text snapshot · Added October 4, 2026

              Week 4 · Post-class memo ↗

              Full main-page text snapshot · Added October 4, 2026

              Week 5 · Testing ↗

              Full main-page text snapshot · Added October 6, 2026

              Week 6 · Auth ↗

              Full main-page text snapshot · Added October 13, 2026

              Homework 2 · instructions ↗

              Linked, not retained. Read October 14, 2026 for chapter HW2, which summarizes it and quotes no homework code.

              Vitest · Mock functions ↗

              Linked, not retained. Used for chapter 7+.

              All six existing guides, readable here

              Open these for the complete original walkthroughs, run instructions, practice prompts, and answer keys. They have been embedded and formatted, so you do not need a Markdown viewer. The original coursework files remain unchanged; readable source snapshots travel with this guide’s folder.

              TypeScript basics · complete original guide

              Original Markdown snapshot · Classes/si-679-f-26-ts-basics-kelvintiger/STUDY_PLAN.md

              TypeScript Basics Study Plan

              This exercise takes a working JavaScript book catalog and adds TypeScript one idea at a time. Read src/index.ts beside this guide.

              Study order
              • 1. Interface: Book describes the required fields and their types. Its

              status must be a BookStatus, so an unrelated string cannot be stored.

              • 2. Typed array: Book[] means the catalog may contain only books.
              • 3. Enum: BookStatus gives names to the three allowed status strings and

              prevents spelling variations throughout the program.

              • 4. Function signatures: parameter and return types document each function

              and let TypeScript check every call.

              • 5. Union type: BookFilter = string | number allows an author or a year.

              The typeof check narrows the union before comparison.

              • 6. Generic: getFirstItem<Item> preserves the item type for any array. A

              book array produces Book | undefined; a string array would produce string | undefined.

              findBookByTitle() and getFirstItem() may return undefined when no item exists. The demo uses optional chaining (?.) so that possibility is handled instead of ignored.

              Exercises and checkpoints
              • Run npm run typecheck. Explain why it prints no output when successful.
              • Run npm run build && npm start and compare every output line with the

              README.

              • Add a practice book with an invalid status string and read the compiler

              error. Remove it afterward.

              • Call findBooksBy() with a boolean and read the compiler error. Remove the

              call afterward.

              • Call getFirstItem() with [1, 2, 3] and hover over the result in your

              editor. TypeScript should infer number | undefined without any.

              The central idea is that TypeScript checks assumptions before the program runs. It does not replace the JavaScript program; the build step produces JavaScript that Node executes.

              Week 1 · complete original guide

              Original Markdown snapshot · Classes/si-679-f-26-week1-nyt-kelvintiger/STUDY_PLAN.md

              Week 1 Study Plan: Express and HTTP from the Beginning

              This guide explains the Week 1 class and the finished assignment in server.js. Work through it in order; each section depends on the one before it.

              What you should be able to do

              By the end of the week, you should be able to:

              • 1. explain the roles of a client, server, request, and response;
              • 2. define GET and POST routes in Express;
              • 3. find data in a query string, route parameters, form body, or JSON body;
              • 4. return a response body and an appropriate HTTP status;
              • 5. validate user input before using it;
              • 6. use Postman or curl to test routes that a browser address bar cannot test.
              1. Build the client/server mental model

              A client asks for something. A server receives the request, runs code, and sends a response.

              text
              client -> HTTP request -> Express route -> HTTP response -> client

              An HTTP request contains a method, a path, headers, and sometimes a body. For example:

              text
              GET /catalog?itemid=222

              GET is the method. /catalog is the path. itemid=222 is a query string. The server matches the method and path to this code:

              js
              app.get('/catalog', (req, res) => {
                // read the request and send a response
              });

              Checkpoint: explain why typing /submitform into a browser address bar cannot test a POST route. The address bar sends GET, while the route expects POST.

              2. Understand the Express setup

              At the top of server.js, express() creates the application. Two middleware functions prepare request bodies before routes use them:

              js
              app.use(express.urlencoded({ extended: true }));
              app.use(express.json());

              express.urlencoded() parses ordinary HTML form submissions. express.json() parses JSON sent by Postman or another program. After parsing, the submitted values are available in req.body.

              At the bottom, app.listen(3000) starts the server. Port 3000 is one numbered door on the computer. The full local address is http://localhost:3000.

              Exercise:

              • 1. Run npm start.
              • 2. Visit / and /about.
              • 3. Change one response string, save, and observe Node's watch mode restart the

              server.

              • 4. Restore the response before continuing.
              3. Learn the four places this assignment receives input
              Query strings

              GET /catalog?itemid=222 places 222 in req.query.itemid. Query strings are good for searches and filters because they do not identify a different route.

              The code verifies that itemid is one string, is not blank, and converts to a number. A bad value returns status 400 and the exact required message.

              Route parameters

              The colons in this route mark changing path segments:

              js
              app.get('/play/artist/:artist/song/:song', ...)

              For /play/artist/Radiohead/song/Creep, Express creates:

              js
              req.params.artist === 'Radiohead'
              req.params.song === 'Creep'

              URL encoding turns %20 into a space, so The%20Clash reaches the handler as The Clash.

              HTML form bodies

              GET /getform returns HTML containing a form. The form's action and method tell the browser to send a POST to /submitform. Each input's name becomes a key in req.body:

              js
              const { address, city, zipcode } = req.body;

              The ZIP code stays a string. That preserves a leading zero such as 01234.

              JSON bodies

              POST /tracks receives JSON. Postman must send Content-Type: application/json, and the server needs express.json() before the route. The route destructures the four fields from req.body and returns a JSON object with the added track and a timestamp.

              Checkpoint: for each assignment route, identify whether its inputs come from req.query, req.params, or req.body.

              4. Understand validation and status codes

              The catalog route returns 400 Bad Request when itemid is not numeric. The track route also returns 400 when artist or title is missing, is not a string, or contains only spaces.

              The pattern is:

              js
              if (inputIsInvalid) {
                res.status(400).send('helpful message');
                return;
              }

              The return matters. Once the server sends an error response, the handler must stop. Otherwise it may try to send a second response.

              Useful first-week statuses:

              • 200: the request succeeded;
              • 400: the client supplied unusable input;
              • 404: no route matches the requested method and path;
              • 500: application code failed unexpectedly.

              Exercise: test /catalog with 222, 3, xyz, a blank value, and no itemid key. Predict the status and body before sending each request.

              5. Walk through every assignment route
              RouteInput sourceMain transformation
              GET /catalogquery itemidvalidate number and place it in a sentence
              GET /play/artist/:artist/song/:songtwo route parametersplace both decoded values in a sentence
              GET /getformnonereturn an address form
              POST /submitformform bodycombine address, city, and ZIP code
              GET /browse/:categoryroute parameter + querycombine category and keywords
              GET /hitherequery namecreate a greeting
              POST /tracksJSON bodyvalidate and return a structured JSON result
              GET /products/:department/:categorytwo route parameters + queryturn comma-separated keywords into AND terms

              The product search calls split(',') to make an array and join(' AND ') to make the required display string. For example:

              text
              lcd,smart,sony -> ['lcd', 'smart', 'sony'] -> lcd AND smart AND sony

              The track response uses new Date().toISOString(). The exact time changes on every request, but the ISO format stays predictable.

              6. Suggested study session
              First 20 minutes: HTTP basics
              • Draw one request and response.
              • Label method, path, query string, headers, body, status, and response body.
              • Explain GET versus POST aloud.
              Next 25 minutes: trace the code
              • Read server.js from top to bottom.
              • For every route, write down where its inputs live.
              • Cover the handler and predict its response from only the request URL/body.
              Next 25 minutes: send requests
              • Create a Week 1 Postman folder.
              • Save one request for each route.
              • Test successful and invalid catalog/track cases.
              • Confirm the status as well as the visible body.
              Final 20 minutes: change and restore
              • Add one temporary route parameter to a practice route.
              • Add one temporary query-string filter.
              • Add one validation check that returns 400.
              • Test each change, then restore the submitted assignment code.
              Final self-check

              You are ready for Week 2 when you can answer these without opening the notes:

              • 1. What is the difference between a method and a path?
              • 2. Where do query parameters, route parameters, and submitted bodies appear

              on req?

              • 3. Why must body-parsing middleware come before the routes?
              • 4. Why does an error branch send a response and then return?
              • 5. What makes an input problem a 400 instead of a 500?
              • 6. Why is Postman useful for POST requests?

              Run the assignment with:

              bash
              npm install
              npm start
              Week 2 · complete original guide

              Original Markdown snapshot · Classes/si-679-f-26-week02-nyt-kelvintiger/STUDY_PLAN.md

              Week 2 Study Plan: Routers, Middleware, and Supertest

              This guide explains the Week 2 class and the finished books assignment. Read the files in this order: types.ts, books-service.ts, books-router.ts, app.ts, index.ts, and finally __tests__/books.test.ts.

              What you should be able to do

              By the end of the week, you should be able to:

              • 1. use TypeScript types in an Express project;
              • 2. split routes into an Express router;
              • 3. separate data operations from HTTP handlers;
              • 4. write middleware and explain why its registration order matters;
              • 5. use headers for simple token checks;
              • 6. choose appropriate HTTP status codes;
              • 7. split the Express app from the listening server;
              • 8. test requests and responses with Vitest and Supertest.
              1. Understand the project layers

              The application separates five responsibilities:

              text
              index.ts -> app.ts -> books-router.ts -> books-service.ts -> catalog array
                                       |
                                       -> types.ts describes the data
              • types.ts defines what a book looks like.
              • books-service.ts owns the in-memory catalog and its operations.
              • books-router.ts translates HTTP requests into service calls.
              • app.ts assembles the Express application and final error handler.
              • index.ts opens port 6790 when the app runs as a real server.

              This separation makes the code easier to understand and lets tests import the app without starting a fixed network listener.

              Checkpoint: explain why a route should call getBook(id) instead of reading the catalog array directly.

              2. Review the TypeScript pieces

              The Book interface requires an ID, title, author, year, and status. BookStatus limits status to the supplied enum values. The service function signatures show what every operation accepts and returns.

              Project-local imports end in .js, even though the source files end in .ts:

              ts
              import { getAllBooks } from './books-service.js';

              TypeScript compiles the files to JavaScript, and Node ultimately loads the .js files. The NodeNext configuration checks imports using those Node ES module rules.

              updateBook() accepts Partial<Omit<Book, 'id'>>:

              • Omit<Book, 'id'> means all book fields except the ID;
              • Partial<...> makes those remaining fields optional.

              That type fits PATCH: a request may change only one field, but it should not replace the book's identity.

              3. Learn the service layer

              books-service.ts keeps the catalog private and exports focused operations:

              FunctionJob
              addBookadd one book
              getAllBooksreturn the catalog
              getBookfind a book by numeric ID
              updateBookapply selected changes to one book
              removeBookremove the matching ID
              _resetCatalogempty test state before each test

              The catalog uses let because removing or resetting books replaces the array. removeBook() uses filter() to keep every book whose ID does not match.

              The leading underscore in _resetCatalog() signals that application routes should not call it. It exists so tests remain independent.

              Exercise: start with three book IDs on paper and walk through removeBook() for the middle ID. Write the array that filter() returns.

              4. Follow middleware in order

              Middleware is a function that receives req, res, and next. It either:

              • calls next() so Express continues; or
              • sends a response and stops the chain.

              The router-wide order is:

              text
              express.json() -> logger -> matching route

              The POST route adds two route-specific steps:

              text
              JSON parser -> logger -> checkAuth -> validateBookParams -> POST handler

              Auth runs before validation. A request with no valid token and a bad book body returns 403 because it stops at checkAuth; validation never runs.

              checkAuth reads the Authorization request header. The expected value is:

              text
              Authorization: Bearer SI679

              This shared string is only a classroom example. A real application would not hard-code a credential in source code.

              validateBookParams requires title and author to be strings containing at least one non-space character. It returns 400 with a helpful message when the submitted book fails that check.

              Checkpoint: what happens if middleware neither sends a response nor calls next()? The request stays open and appears to hang.

              5. Walk through the API
              RequestAuth?Result
              POST /booksyesvalidate, create a book, return 201 and JSON
              GET /booksnoreturn all books or exact-match filters
              GET /books/:idnoreturn one book or 404
              PATCH /booksyesupdate selected fields, 400 for bad input, 404 if absent
              DELETE /booksyesremove the ID and return 200
              GET /books/badroutenothrow an example error handled as 500
              Query filtering

              GET /books checks title, author, and year. Every supplied filter must match, so author and year together act like an AND condition. Query values are strings; the code converts year to a number before comparing it with a book's numeric year.

              GET by ID

              Route parameters are strings. Number(req.params.id) converts the route value before the service compares it with a numeric book ID.

              POST status

              The assignment changes a successful POST from the default 200 to 201 Created. The response contains the created book, including its generated ID and default available status.

              PATCH validation

              The body must contain an id plus at least one field to change. The router separates them with:

              ts
              const { id, ...changes } = req.body;

              changes contains everything except id. The service applies those fields to the existing book with Object.assign().

              6. Understand the app/server split and error handler

              app.ts creates and exports the Express app. index.ts imports it and calls listen(). Supertest imports app.ts, so it can make requests without asking you to start the server first.

              The final error handler has four parameters:

              ts
              (err, req, res, next)

              Express uses that four-parameter shape to recognize error middleware. It is registered after the router because Express looks forward through the chain when a handler throws.

              Checkpoint: move the error handler above the router in a drawing. Why would an error thrown later fail to reach it?

              7. Read the tests as examples of API use

              Supertest builds a request with a readable chain:

              ts
              await request(app)
                .post('/books')
                .set('Authorization', 'Bearer SI679')
                .send({ title: 'The Hobbit', author: 'J.R.R. Tolkien', year: 1937 });

              The test then checks res.status, res.body for JSON, or res.text for a plain text response. The Week 2 tests cover:

              • empty initial state;
              • exact author filtering;
              • missing and incorrect tokens;
              • blank-title validation and its message;
              • POST creation and 201;
              • GET by ID and missing-ID 404;
              • PATCH auth, input validation, partial update, and unchanged fields;
              • DELETE auth and final empty state.

              beforeEach() calls _resetCatalog(). Without it, a book posted by one test would remain for the next test and make the outcome depend on test order.

              8. Suggested study session
              First 20 minutes: draw the architecture
              • Draw all five source-file layers.
              • Follow one POST request from app.ts through middleware and the service.
              • Follow the response back to the client.
              Next 25 minutes: middleware practice
              • Write the ordered chain for GET, POST, PATCH, and DELETE.
              • Predict which status wins for a request with both bad auth and bad data.
              • Use Postman to compare no token, the wrong token, and the correct token.
              Next 25 minutes: route behavior
              • POST two books by different authors.
              • Filter by one author, then by author plus year.
              • GET one returned ID.
              • PATCH its title and verify its author did not change.
              • DELETE it and confirm it is absent.
              Final 20 minutes: tests
              • Run npm test.
              • Read one test at a time using arrange, act, assert:

              set up data, send the request, check the result.

              • Temporarily remove _resetCatalog() and observe how shared state changes the

              suite. Restore it before finishing.

              Final self-check

              You are ready for Week 3 when you can answer these without opening the code:

              • 1. Why use a router instead of putting every route in index.ts?
              • 2. What are the two valid ways for middleware to finish its work?
              • 3. Why does middleware order change the response?
              • 4. What do 201, 400, 403, 404, and 500 mean here?
              • 5. Why are app.ts and index.ts separate?
              • 6. Why does every test reset the catalog?
              • 7. When should a test inspect res.body versus res.text?

              Useful commands:

              bash
              npm run typecheck
              npm test
              npm run build
              npm run dev
              HW1 · SliceDrop · complete original guide

              Original Markdown snapshot · Homework/si-679-f-26-hw1-kelvintiger/TEACHING_GUIDE.md

              SliceDrop HW1: A Beginner's Guide to the Finished API

              This guide explains the completed SliceDrop menu and orders API from the ground up. It assumes that HTTP, Express, TypeScript, and automated tests are all new. The examples and descriptions match the code and checked-in tests in this repository.

              The assignment is small on purpose. It gives one application enough pieces to show the Week 1 and Week 2 ideas working together:

              • a client sends HTTP requests;
              • an Express server receives them and runs middleware in order;
              • routers choose the behavior for a path;
              • plain functions validate data and look up menu items;
              • a small in-memory data module stores orders;
              • the server returns a status code, headers, and a JSON body;
              • Vitest and Supertest check the observable behavior.
              1. The mental model: a client talks to a server through an API

              An API is a set of agreed request and response shapes. A client can be a web page, a mobile app, Postman, curl, or a test. The client does not call TypeScript functions such as addOrder directly. It sends an HTTP request.

              text
              client
                │  HTTP request: method, URL, headers, optional JSON body
                ▼
              Express application (`app`)
                │  body parser → router → auth middleware → handler → data/validation
                ▼
              HTTP response: status, headers, JSON body
                │
                ▼
              client reads the result

              For example, a customer placing an order sends:

              http
              POST /orders HTTP/1.1
              Authorization: Bearer slicedrop-customer-secret
              Content-Type: application/json
              
              {"customerName":"Ada","items":[{"menuItemId":"soda","quantity":1}]}

              The server parses the JSON body, checks the token, validates the order, stores it in memory, and answers with 201 Created and a JSON representation of the new order. The client only needs the API contract: it does not need to know which array or function stored the order.

              This project has no database, browser UI, or deployed service. The API is the Express application. The index.ts file is the part that opens a network port when the application is run as a server.

              2. HTTP pieces used by this API
              Methods and routes

              An HTTP method says what kind of operation the client is requesting. A route is the method plus the path pattern. The same path can have different behavior for different methods; GET /orders and POST /orders are separate routes.

              Method and pathWho may call itBehavior
              GET /menuanyoneReturn all six menu items, or filter by category.
              GET /menu/:idanyoneReturn one menu item by its exact ID, or 404.
              POST /orderscustomer tokenValidate and create an order, or return validation errors.
              GET /ordersstaff tokenReturn all stored orders, or filter by status.

              The :id in GET /menu/:id is a route parameter. A request for /menu/hawaiian makes req.params.id equal to the string "hawaiian".

              Status codes

              The status code is a compact description of what happened.

              • 200 OK means the request succeeded. It is used for menu responses and

              successful order listings.

              • 201 Created means a new resource was created. A valid POST /orders

              returns this status.

              • 400 Bad Request means the client sent something the server cannot process,

              such as invalid JSON or an invalid order body.

              • 401 Unauthorized means the request did not present the required valid

              token. This assignment uses 401 for a missing, malformed, unknown, or wrong-role token.

              • 404 Not Found means no route handled the path, or a requested menu ID does

              not exist.

              • 500 Internal Server Error means application code failed unexpectedly. It

              is a server problem, even when it happens while handling a valid request.

              The status is part of the API contract. A client can make a useful decision from 201, 400, or 401 even before reading the body.

              Headers

              Headers are named metadata attached to an HTTP request or response. This API uses two request headers especially:

              • Authorization: Bearer <token> carries the customer or staff credential.
              • Content-Type: application/json tells the server that the body is JSON.

              express.json() uses the content type and JSON parser to turn a valid JSON request body into req.body. If JSON cannot be parsed, the parser raises an error before an order handler runs.

              Body, query string, and route parameters

              These are three different places request data can live:

              text
              GET /menu/hawaiian?category=pizza
                  └──────┬─────┘ └──────┬──────┘
               route parameter `id`      query parameter `category`
              • The body is normally used for data being submitted. The order body contains

              customerName and items.

              • A query string begins after ?. GET /menu?category=pizza puts the value

              in req.query.category; GET /orders?status=pending puts the value in req.query.status. Query filters do not create a missing-resource error: an unknown category or status produces 200 with [].

              • A route parameter is part of the path pattern. In /menu/:id, the value is

              available at req.params.id.

              Week 1 introduced these ideas with small app.get and app.post handlers. HW1 applies the same ideas after the handlers have been organized into routers.

              3. The project map and the app/server split

              The important files fit together like this:

              text
              src/index.ts                 starts listening on port 3000
              src/app.ts                   builds and exports the Express app
              src/routers/menu.ts          GET /menu and GET /menu/:id
              src/routers/orders.ts        POST /orders and GET /orders
              src/middleware/auth.ts       Bearer parsing and role checks
              src/validation/validate-order.ts
                                            order-body validation
              src/data/menu.ts             supplied menu array and lookup function
              src/data/orders.ts           in-memory order array and ID generation
              src/types.ts                 shared TypeScript types
              src/constants.ts             tokens and port
              src/__tests__/*.test.ts      Vitest/Supertest behavior checks

              src/app.ts exports app but does not call listen. src/index.ts imports that app and starts the real server:

              ts
              import { app } from "./app";
              import { PORT } from "./constants";
              
              app.listen(PORT, () => {
                console.log(`SliceDrop listening on http://localhost:${PORT}`);
              });

              This split is important for testing. Supertest can import the app and manage a temporary listener for each test run, so the developer does not have to start the server on its normal fixed port first. Calling listen in app.ts would start that fixed listener whenever the module was imported and could cause port conflicts.

              The supplied constants.ts gives the exact development values:

              text
              customer token: slicedrop-customer-secret
              staff token:    slicedrop-staff-secret
              port:           3000

              These hard-coded tokens are suitable only for this exercise. The comments in that file point toward a later course topic: real secrets should come from configuration such as environment variables, rather than source code.

              4. The middleware chain: order is behavior

              Express processes a request by walking through the middleware and route handlers in the order they were registered. Each ordinary middleware has the shape (req, res, next):

              • inspect or change the request;
              • send a response and stop, or call next() to continue;
              • never do neither, or the request can hang.

              The finished app.ts registers the shared chain in this order:

              text
              1. express.json()
              2. /menu router
              3. /orders router
              4. JSON 404 handler
              5. four-argument error handler

              The order matters in several ways.

              JSON parsing must be before routes

              The order handlers expect req.body to contain parsed JSON. Therefore express.json() must be registered before both routers. A valid JSON body is available to the order handler; malformed JSON causes the parser to pass an error to the final error handler.

              Authorization must be before order validation

              POST /orders has this route-specific chain:

              text
              express.json()
                → orders router matches `/`
                → requireCustomerToken
                → POST handler
                → validateOrder
                → addOrder

              The customer middleware is placed before the handler, so a request with no token gets 401 without being validated. The test named “auth runs before validation” deliberately sends {} with no token and expects 401, not 400. That result is a consequence of registration order, not a special case inside the validator.

              GET /orders follows the same pattern with requireStaffToken before the listing handler. A valid customer token is still rejected there because the route requires the staff token.

              404 must be after the routers

              The 404 handler is a normal fall-through middleware. If it came before the routers, it would answer every request and the routers would never run. After the routers, it means “no earlier route matched.” It returns:

              json
              {"error":"Not found"}
              The error handler must be last and have four parameters

              Express identifies error-handling middleware by the exact four-parameter form:

              ts
              (err, req, res, next)

              The finished handler is registered after the 404 handler. The next parameter is not used in this assignment, but it must remain in the function signature so Express recognizes the function as an error handler.

              5. Authentication and exact Bearer parsing

              The authentication module separates token parsing from Express middleware. That is a useful Week 2 design choice: the parser is a plain function that can be tested without constructing a request, while the middleware handles the HTTP response and next().

              What counts as a valid header

              parseBearerToken(header) accepts only the exact shape:

              text
              Bearer <one-or-more-non-whitespace-characters>

              The implementation uses the case-sensitive regular expression /^Bearer ([^\s]+)$/.

              Header valueResult
              missing/undefinednull
              Bearer slicedrop-customer-secrettoken string
              bearer slicedrop-customer-secretnull because Bearer is case-sensitive
              Basic slicedrop-customer-secretnull
              Bearer null because the token is empty
              Bearer one twonull because the token contains whitespace
              Bearer nope"nope"; parsing succeeds, but authorization rejects it

              The parser does not decide whether the token is customer or staff. It only extracts a candidate string or returns null.

              What the two middleware functions do

              requireCustomerToken reads req.headers.authorization, passes it to the parser, and compares the result to CUSTOMER_TOKEN. If it does not match, it sends:

              http
              401 Unauthorized
              json
              {"error":"Unauthorized"}

              On a match it calls next(), allowing validation and order creation to run. requireStaffToken has the same structure but compares with STAFF_TOKEN.

              The distinction is intentional: possession of a valid customer token does not grant access to the staff-only order list.

              6. The menu API
              GET /menu

              With no query parameter, the menu router returns the supplied menu array directly as JSON. It contains six items:

              text
              hawaiian, meat-lovers, build-your-own,
              bread-nugz, dipping-sauce, soda

              With ?category=pizza, the handler uses menu.filter(...) and keeps items whose category exactly equals "pizza". The result contains three pizzas.

              http
              GET /menu?category=pizza
              json
              [
                {"id":"hawaiian","name":"Hawaiian","category":"pizza", "...":"..."},
                {"id":"meat-lovers","name":"Meat Lovers","category":"pizza", "...":"..."},
                {"id":"build-your-own","name":"Build Your Own","category":"pizza", "...":"..."}
              ]

              The "..." markers above mean additional real fields are present; they are not literal response fields. A category with no matches, such as category=seafood, returns 200 and []. If a query value is not a string (for example, a repeated or unusually shaped query parameter), the handler also returns [] rather than treating it as an exact category.

              GET /menu/:id

              The handler reads req.params.id and calls findMenuItem(id). That supplied function uses menu.find(...) and returns either a MenuItem or undefined.

              For a known ID:

              http
              GET /menu/hawaiian

              the response is 200 and includes the full item, including its three sizes, crust choices, and toppings. For an unknown ID:

              http
              GET /menu/sushi

              the response is:

              http
              404 Not Found
              json
              {"error":"Menu item not found"}

              Returning [] would be appropriate for an empty collection filter, but a single requested resource that does not exist is represented by 404.

              7. Orders: creating and listing data
              POST /orders

              The request first needs the customer token. Once authorized, the handler calls validateOrder(req.body). An empty error list means the body is valid. A non-empty list is returned as:

              http
              400 Bad Request
              json
              {
                "errors": [
                  "customerName must be a non-blank string",
                  "items must be a non-empty array"
                ]
              }

              For valid input, addOrder(req.body) creates and stores the order, and the handler returns 201:

              http
              POST /orders
              Authorization: Bearer slicedrop-customer-secret
              Content-Type: application/json
              json
              {
                "customerName": "Ada",
                "items": [
                  {"menuItemId": "hawaiian", "quantity": 2, "size": "large"}
                ]
              }
              http
              201 Created
              json
              {
                "id": "1",
                "status": "pending",
                "createdAt": "2026-09-16T12:00:00.000Z",
                "customerName": "Ada",
                "items": [
                  {"menuItemId": "hawaiian", "quantity": 2, "size": "large"}
                ]
              }

              The timestamp in a real response is the current time, so tests check that it is a parseable string instead of comparing one fixed value. IDs are strings and increase from "1"; each call receives a distinct ID while the process is running. Every new order starts with status "pending".

              GET /orders

              This endpoint requires the staff token:

              http
              GET /orders
              Authorization: Bearer slicedrop-staff-secret

              With no filter it returns the current array in insertion order. With ?status=pending, it calls listOrders("pending") and returns only matching orders. An unknown or currently empty status returns 200 and []; it is not an invalid request.

              The order data is deliberately in memory. src/data/orders.ts starts with:

              ts
              let orders: Order[] = [];
              let nextId = 1;

              addOrder creates an Order, increments nextId, pushes the order, and returns it. listOrders either returns all orders or uses filter for a status. _resetOrders() clears the array and resets IDs for tests. The orders are lost and IDs start over when the Node process restarts; this is not a persistent database.

              8. Order validation, including every error

              validateOrder(body) is a plain function returning string[]. It does not send an HTTP response. The route handler decides that a non-empty array means 400. Keeping validation separate makes the rules easier to read and test.

              The function is defensive because req.body comes from an outside client. A client can send null, a number, an array, or an object with unexpected field types. The implementation checks before reading nested properties.

              Request-level checks
              • 1. It checks whether body is a non-null object and not an array. If not, the

              values used for validation are treated as undefined.

              • 2. customerName must be a string whose trim() is not empty. This rejects a

              missing value, a number, "", and whitespace-only text.

              • 3. items must be a non-empty array. If it is not an array, the function

              returns the errors found so far rather than calling .forEach on an unsafe value.

              Item-level checks

              For every item, the validator checks:

              • the item is object-like before reading its fields;
              • menuItemId is a string identifying an existing menu item;
              • quantity is an integer at least 1;
              • an optional size is offered by that menu item;
              • an optional crust is offered by that menu item;
              • an optional sauce is offered by that menu item;
              • an optional toppings value is an array, and every topping name is offered.

              The validator uses the item index in messages, such as items[0].quantity must be a whole number of at least 1. It does not stop at the first problem. For example, an order with a blank name, an unknown menu ID, and quantity 0 receives all applicable messages in the same errors array. This is more useful to a client than forcing three separate requests.

              Option checks happen only when the menu item was found. That avoids trying to read option lists from undefined. It also means an unknown menu item gets the menu-ID error without a misleading size, crust, sauce, or topping error.

              The four supplied helper functions at the bottom of the file implement the membership checks:

              • offersSize returns false when sizes is absent, then uses .some to

              compare an option's name.

              • offersCrust returns false when crusts is absent, then uses .includes.
              • offersSauce does the same for sauces.
              • offersTopping returns false when toppings is absent, then uses .some

              to compare topping names.

              Matching is exact and case-sensitive. "Large" is not the same as "large". The soda item has sizes but no crusts or toppings; a supplied unsupported option therefore fails validation.

              9. TypeScript ideas in this code
              Interfaces and type aliases

              src/types.ts describes the shapes shared across modules.

              • Category is a union of four allowed strings: "pizza", "appetizer",

              "side", and "beverage".

              • SizeOption and ToppingOption require a name and numeric price.
              • MenuItem requires id, name, and category; description, price,

              sizes, crusts, sauces, and toppings are optional with ?.

              • OrderStatus is the union "pending" | "in-progress" | "completed".
              • OrderItem requires menuItemId and quantity, while customization fields

              are optional.

              • NewOrder is the customer-submitted shape.
              • Order extends NewOrder, so an Order has all of the new-order fields plus

              id, status, and createdAt.

              An interface describes an object shape; a type alias such as Category or OrderStatus can describe a set of allowed literal values. TypeScript checks these shapes while compiling, but a client can still send bad runtime data. That is why validateOrder still performs real runtime checks.

              undefined and narrowing

              Optional properties may be absent, so TypeScript represents their values as possibly undefined. findMenuItem returns MenuItem | undefined because a lookup may fail. The code narrows before using the value:

              ts
              const menuItem = findMenuItem(id);
              if (menuItem === undefined) {
                res.status(404).json({ error: "Menu item not found" });
                return;
              }
              
              res.json(menuItem); // here TypeScript knows it is a MenuItem

              The validator uses checks such as Array.isArray(items), typeof category === "string", and menuItem === undefined to narrow broad runtime values into safe, more specific ones. The early return statements make those narrowed facts easy to follow.

              validateOrder(body: any) intentionally accepts any incoming body so it can inspect malformed values. any turns off compile-time protection for that value, so the function compensates with explicit runtime guards. The topping callback uses unknown, which is safer: a value of type unknown must be checked before treating it as a string.

              Arrays and callback methods

              Several small array methods express the business rules directly:

              ts
              menu.find((item) => item.id === id);       // one matching item or undefined
              menu.filter((item) => item.category === category); // all matches
              items.forEach((item, index) => { ... });   // validate every item
              options.some((option) => option.name === size); // any option matches
              crusts.includes(crust);                    // exact string membership

              The tests also use .map to project order names and .every to verify that a filtered result satisfies a condition. These methods take callback functions: the arrow function receives an element, and the method returns the appropriate result without requiring a manual index loop.

              10. File-by-file walkthrough of the completed work

              This section follows the changed files from the outside of the application toward the inside.

              src/app.ts
              • 1. The imports bring in Express request/response types, the two routers, and

              the types needed by the error handler.

              • 2. export const app = express() creates the application object and exports it

              for both index.ts and tests.

              • 3. app.use(express.json()) installs the body parser before any route.
              • 4. app.use("/menu", menuRouter) mounts the menu router at the /menu base

              path. app.use("/orders", ordersRouter) does the same for orders.

              • 5. The next middleware returns JSON 404 for any request that got through

              both routers without a match.

              • 6. The final four-argument middleware distinguishes malformed JSON from other

              errors. A SyntaxError with a body property is answered with 400 and { error: "Request body must be valid JSON" }. Any other error is logged with console.error and answered with 500 and { error: "Internal server error" }.

              The return statements after responses are control-flow guards. They stop the handler from trying to send a second response.

              src/middleware/auth.ts
              • 1. It imports Express types and the two token constants.
              • 2. parseBearerToken handles only extraction. It returns null for a missing

              header or a regex mismatch and returns capture group 1 for a valid header.

              • 3. requireCustomerToken calls the parser, sends 401 on any non-matching

              token, and calls next() only for the customer token.

              • 4. requireStaffToken repeats that pattern for the staff token.

              The parser is deliberately not middleware. A plain function has no response to send and no next() to call, so its job stays focused and direct.

              src/routers/menu.ts
              • 1. It imports Express request/response/router types and the supplied menu data

              functions.

              • 2. menuRouter = Router() creates a router whose paths are relative to the

              mount point in app.ts.

              • 3. menuRouter.get("/", ...) handles the mounted GET /menu. It returns the

              whole array when category is absent, returns [] when the query value is not a string, and otherwise filters for an exact category.

              • 4. menuRouter.get("/:id", ...) handles mounted GET /menu/:id. It looks up

              the ID, returns a 404 error object when there is no item, and returns the item when there is one.

              Because the router is mounted at /menu, its internal / is not just / to the client. The combination is /menu/ (and Express also handles the usual slash variation), while its internal /:id becomes /menu/:id.

              src/routers/orders.ts
              • 1. It imports the supplied addOrder and listOrders functions, both auth

              middleware functions, and validateOrder.

              • 2. ordersRouter = Router() creates a relative router.
              • 3. ordersRouter.post("/", requireCustomerToken, handler) puts customer auth

              before the handler. The handler collects validation errors, returns 400 with { errors } when needed, and otherwise calls addOrder and returns 201 with the new order.

              • 4. ordersRouter.get("/", requireStaffToken, handler) puts staff auth before

              the listing logic. The handler returns all orders without a status query, returns [] for a non-string query value, and passes a string status to listOrders.

              The leading slash in each router path is relative to the /orders mount, so the public paths are POST /orders and GET /orders.

              src/validation/validate-order.ts
              • 1. It imports the MenuItem type and findMenuItem.
              • 2. It creates an error accumulator, checks whether the body is a non-array

              object, and validates customerName.

              • 3. It validates that items is a non-empty array. If not, it returns the

              accumulated errors safely.

              • 4. It loops through every item, looks up the menu ID, checks quantity, and

              returns the menu-ID and quantity errors as appropriate.

              • 5. When an item and menu item are both safe to inspect, it checks each optional

              customization by calling the supplied helper functions.

              • 6. It returns the complete errors array. An empty array is the success signal.
              • 7. The four helpers at the bottom handle option membership and safely treat an

              absent option list as “not offered.”

              The function deliberately accumulates with errors.push(...) instead of returning immediately from the first bad field. It only returns early when continuing would be unsafe, such as when items is not an array.

              src/types.ts

              This supplied module is the shared vocabulary. It prevents a menu item from silently becoming an arbitrary object inside the typed parts of the program, documents which fields are optional, and distinguishes a customer-submitted NewOrder from a stored Order with server-generated fields.

              src/data/menu.ts

              This supplied module defines the six menu items and reusable constants for standard toppings, crusts, and dipping sauces. findMenuItem uses .find and can return undefined. The data is process-local and is not changed by the API routes.

              src/data/orders.ts

              This supplied module is the exercise's temporary database. addOrder creates the server fields, increments the ID counter, pushes into the array, and returns the object. listOrders preserves insertion order when unfiltered and uses .filter for a status. _resetOrders is test-only and is never called by application routes.

              src/constants.ts and src/index.ts

              constants.ts holds the two tokens and port. index.ts is the executable entrypoint that starts listening. Keeping this start-up action out of app.ts is what lets Supertest import the app safely.

              11. How the tests map to requirements

              Vitest supplies describe, it, beforeEach, expect, and test doubles such as vi. Supertest supplies a client-like API:

              ts
              const res = await request(app)
                .post("/orders")
                .set("Authorization", `Bearer ${CUSTOMER_TOKEN}`)
                .send(ORDER);
              
              expect(res.status).toBe(201);
              expect(res.body.status).toBe("pending");

              request(app) gives the exported Express app to Supertest. It does not require the developer to run npm run dev or start port 3000; Supertest manages the temporary listener used for the request. .set adds a header, .send supplies a JSON body, and Supertest parses JSON responses into res.body.

              src/__tests__/auth.test.ts
              • verifies that missing and unknown customer tokens produce 401;
              • verifies that the customer token allows POST /orders;
              • verifies that missing, unknown, and customer tokens are rejected by

              GET /orders;

              • verifies that the staff token allows the order list;
              • verifies that authentication runs before validation by expecting 401 for a

              missing token and bad body.

              src/__tests__/errors.test.ts
              • verifies a path no router handles returns 404 with an error field;
              • verifies invalid orders return 400 with an errors array;
              • verifies the validator catches unknown menu IDs, quantities below one,

              unsupported crusts, and unsupported sizes;

              • mocks addOrder to throw and verifies an application failure becomes 500,

              not a client-facing 400;

              • sends malformed JSON and verifies the parser error becomes a JSON 400.

              The mock test is especially useful because it distinguishes two error sources: the client sent valid JSON and a valid order, but application code still failed.

              src/__tests__/menu.test.ts
              • checks that the unfiltered menu has six items;
              • checks exact category filtering for pizzas;
              • checks an unknown category returns an empty successful array;
              • checks a known item includes customization options;
              • checks an unknown ID returns 404 with an error field.
              src/__tests__/orders.test.ts
              • checks successful creation returns 201, preserves customer and items,

              starts as pending, gives an ID, and creates an ISO-parseable timestamp;

              • checks two orders receive different IDs;
              • checks an empty order list before creation;
              • checks status filtering and the empty result for a status with no matches;
              • checks that unfiltered results preserve insertion order.

              Each order test file calls _resetOrders() in beforeEach. That keeps tests independent even though the application data module is stateful. Tests should not rely on another test having run first.

              The timestamp assertion demonstrates a general testing rule: assert a stable property, such as “is a parseable date string,” rather than an unstable exact value, such as the clock time from one particular run.

              12. Install, run, test, type-check, and build

              Run these commands from the repository directory:

              bash
              npm install
              npm test
              npm run typecheck
              npm run build

              What each command does:

              • npm install installs the dependencies in package.json and records them

              through the lockfile.

              • npm test runs vitest run, which executes the test suite once.
              • npm run test:watch runs Vitest interactively and reruns tests as files

              change.

              • npm run typecheck runs tsc --noEmit. It checks types without creating

              output files.

              • npm run build runs tsc -p tsconfig.build.json and emits compiled server

              code into dist while excluding tests.

              • npm run dev runs tsx src/index.ts, starts the server on port 3000, and

              prints its local URL. tsx runs TypeScript directly; it does not replace the separate type-check command.

              With the development server running, equivalent manual requests include:

              bash
              curl http://localhost:3000/menu
              curl 'http://localhost:3000/menu?category=pizza'
              curl http://localhost:3000/menu/hawaiian
              
              curl -X POST http://localhost:3000/orders \
                -H 'Authorization: Bearer slicedrop-customer-secret' \
                -H 'Content-Type: application/json' \
                -d '{"customerName":"Ada","items":[{"menuItemId":"soda","quantity":1}]}'
              
              curl http://localhost:3000/orders \
                -H 'Authorization: Bearer slicedrop-staff-secret'

              The last two requests must occur in the same running process if the listing is expected to show the newly created order. Restarting the process clears the in-memory order array.

              The repository README says the short workflow is to install dependencies and make the tests pass, and that the live assignment specification is the primary source when available. The checked-in tests are the concrete, executable examples of the behavior described here.

              13. Common mistakes and a debugging routine
              Common implementation mistakes
              • 1. Registering express.json() too late. Put it before both routers so

              req.body is ready.

              • 2. Putting auth inside validation or after the handler. Auth belongs as the

              route-specific middleware argument before the handler. Otherwise a missing token can incorrectly produce 400 or create an order.

              • 3. Using the wrong token for a role. The customer token is valid only for

              POST /orders; the staff token is required for GET /orders.

              • 4. Accepting loose Bearer formats. The required parser is exact: capital

              Bearer, one literal separating space, and a non-whitespace token with no extra text.

              • 5. Mounting paths twice. Once the app mounts menuRouter at /menu, the

              router should define / and /:id, not /menu and /menu/:id.

              • 6. Returning 200 for a missing single item. Unknown /menu/:id values

              are 404; unknown collection filters are 200 with [].

              • 7. Stopping validation at the first error. Keep pushing all discovered

              problems into the array.

              • 8. Calling .forEach before checking items. Check Array.isArray(items)

              first. Likewise, check that a menu lookup succeeded before reading option lists.

              • 9. Confusing malformed JSON with an invalid order. Malformed JSON is a

              parser error and gets 400 from the global handler. Valid JSON with bad fields gets 400 with { errors: [...] }. A thrown application exception gets 500.

              • 10. Forgetting to return after sending. A handler that sends an error and

              keeps running can attempt a second response and trigger another error.

              • 11. Removing the fourth error-handler parameter. Express uses the four

              parameters to recognize error middleware, even when next is unused.

              • 12. Expecting orders to survive a restart. The array is in memory. Restart

              means an empty list and IDs beginning again at "1".

              • 13. Testing an exact timestamp. Check its type and parseability, as the

              supplied test does.

              • 14. Relying on test order. Reset state in beforeEach; a test should be

              able to run by itself.

              A practical debugging routine

              When a request fails, inspect the problem in this order:

              • 1. Confirm the method and full path. POST /orders and GET /orders are

              different, and a router path is relative to its mount point.

              • 2. Check the status code before reading the body. 401 points to auth, 404

              to routing or an unknown menu ID, and 400 to parsing or validation.

              • 3. Check the headers. For orders, use the exact token and

              Content-Type: application/json.

              • 4. Check the body shape. customerName must be non-blank and items must be

              a non-empty array; each item needs a known menu ID and integer quantity.

              • 5. Add a focused test or run one existing test by its description. A failing

              test tells you the observable contract rather than an internal guess.

              • 6. If the status is 500, read the server log. The global handler logs the

              unexpected error while returning a deliberately generic client message.

              • 7. Run npm run typecheck after fixing behavior. Passing tests alone does not

              prove that TypeScript's compile-time checks pass.

              When the result seems surprising, trace the chain from the top: JSON parser, mount point, router path, route-specific middleware, handler, data function, then response. That follows the order Express actually uses.

              14. Self-check quiz

              Try to answer these without looking at the answer key.

              • 1. What is the difference between the exported app and the server started by

              index.ts?

              • 2. Which middleware makes a valid JSON body available as req.body?
              • 3. Why must that middleware be registered before the routers?
              • 4. What public path is produced when app.ts mounts menuRouter at /menu

              and the router defines get("/:id", ...)?

              • 5. Which token and method are required to create an order?
              • 6. What does parseBearerToken("Bearer nope") return, and where is the token

              accepted or rejected afterward?

              • 7. What response should a request with no token and body {} receive for

              POST /orders?

              • 8. What is the difference between an unknown category and an unknown menu ID?
              • 9. Why does validateOrder return an array instead of immediately responding?
              • 10. Name three conditions that make quantity invalid.
              • 11. Why does the validator check menuItem === undefined before checking a

              size or topping?

              • 12. What does Order extends NewOrder communicate?
              • 13. Why does the error handler need four parameters if it does not call

              next()?

              • 14. How does the application distinguish malformed JSON from an application

              exception?

              • 15. What happens to orders when the Node process restarts?
              • 16. Why do order tests call _resetOrders() before each test?
              • 17. Which command runs tests once, and which command checks types without

              emitting JavaScript?

              • 18. What status should a successful order creation return, and why is that more

              specific than 200?

              Answer key
              • 1. app.ts constructs and exports the Express app for reuse; index.ts

              imports it and calls listen on port 3000.

              • 2. express.json().
              • 3. It must parse the body before handlers that read req.body; its position in

              the chain controls whether the routes see parsed data.

              • 4. GET /menu/:id, such as GET /menu/hawaiian.
              • 5. POST /orders requires the exact customer token

              slicedrop-customer-secret in Authorization: Bearer ... form.

              • 6. It returns the string "nope"; the customer or staff middleware compares

              that candidate with its required constant and rejects it with 401.

              • 7. 401 Unauthorized, because auth runs before validation.
              • 8. An unknown category is an empty successful collection (200, []); an

              unknown single menu ID is 404 with an error object.

              • 9. A plain function can be tested and reused independently of Express; the

              route handler translates its returned errors into an HTTP response.

              • 10. It is missing, not a number, not an integer, zero, or negative. Fractions

              are also invalid.

              • 11. A failed lookup returns undefined, so reading option properties would be

              unsafe and could cause a server exception.

              • 12. A stored Order has all NewOrder fields plus server-generated id,

              status, and createdAt.

              • 13. Express recognizes error middleware by (err, req, res, next); the fourth

              parameter is part of that recognition rule.

              • 14. The handler checks for SyntaxError with a body property and returns

              400; all other errors are logged and return generic 500.

              • 15. The in-memory array disappears, so orders are lost and the next ID starts

              at "1" in the new process.

              • 16. To clear shared in-memory state and keep tests independent of execution

              order.

              • 17. npm test runs the suite once; npm run typecheck runs tsc --noEmit.
              • 18. 201 Created, because the request created a new order resource and the

              status communicates that fact to the client.

              If the answer to any question was uncertain, trace the matching request through the middleware chain and then read the corresponding test. That is the same method used to understand the rest of this API: identify the request, follow the ordered handlers, and inspect the status and JSON response.

              Week 3 · complete original guide

              Original Markdown snapshot · Classes/si-679-f-26-week03-nyt-kelvintiger/STUDY_PLAN.md

              Week 3 Study Plan: MongoDB, Express, and API Tests

              Use this guide with the completed code in src/. It starts with the ideas behind the code, then walks through each file and gives you small checks to make sure you can explain what is happening.

              What you should be able to do

              By the end of this week, you should be able to:

              • 1. explain how a MongoDB collection and document compare with a SQL table and row;
              • 2. connect a TypeScript program to a MongoDB database;
              • 3. create, read, update, and delete documents;
              • 4. place database functions behind Express routes;
              • 5. recognize a MongoDB ObjectId and reject a malformed one;
              • 6. use Vitest, Supertest, and MongoMemoryServer to test an API without changing a real database;
              • 7. use test hooks to give every test a clean starting state.
              1. Start with the database vocabulary

              MongoDB is a document-oriented NoSQL database. A useful first comparison is:

              MongoDBRough SQL comparisonExample in this project
              databasedatabaseweek3app or the test database
              collectiontableproducts
              documentrowone Duct Tape product
              fieldcolumnname, price, or quantity

              A product document looks much like a JavaScript object:

              ts
              {
                name: 'Duct Tape',
                price: 5.99,
                quantity: 120
              }

              MongoDB adds an _id field when the document is inserted. The value is an ObjectId. Its printed form is a string of 24 hexadecimal characters, but the database value is an ObjectId, not an ordinary string.

              Checkpoint:

              • Which collection stores the records in this app?
              • Which part uniquely identifies one product?
              • Why must new ObjectId(id) be used before searching by _id?
              2. Practice Mongo operations directly

              Read src/db-explore.ts first. It is a scratch program for learning the database driver before Express is involved.

              The connection sequence is:

              text
              MongoClient(uri) -> client.connect() -> client.db(name) -> collection(name)

              The file includes these four CRUD operations:

              GoalMongoDB method
              create documentsinsertMany()
              read documentsfind(), findOne()
              update documentsupdateOne(), updateMany()
              delete documentsdeleteOne()

              Mongo queries are objects. { name: 'Masking Tape' } means “documents whose name equals Masking Tape.” { price: { $gte: 5 } } means “documents whose price is greater than or equal to 5.” The $set update operator changes only the fields named inside it.

              The four Now You Try functions demonstrate those ideas:

              • findPricierTest() uses $gte to find products costing at least $5.
              • updatePriceTest() changes only Scotch Tape's price.
              • deleteByNameTest() deletes a document using a name query.
              • markOnSaleTest() uses updateMany() to add onSale: true to every

              product costing less than $5.

              Exercise:

              • 1. Start local MongoDB.
              • 2. Uncomment await addProducts() in main() and run npm run explore once.
              • 3. Comment it again so repeated saves do not create duplicates.
              • 4. Run each Now You Try function one at a time and print the result.
              • 5. After each operation, print getAllProducts() and predict the output before

              looking at it.

              3. Separate database work from HTTP work

              src/db.ts is the database layer used by the application. It keeps the MongoDB driver details in one place. Express routes do not need to know how a client is created or which collection method performs an update.

              The public functions form a small interface:

              FunctionInputOutput
              connectURI and database namean open connection
              getAllProductsnoneevery product document
              getProductstring IDone document or null
              addProducta Productthe new ObjectId
              updateProductID and partial changesnumber of matched documents
              deleteProductIDnumber of deleted documents

              ProductUpdate makes each property optional. That matters because a PATCH request can change only price while leaving name and quantity alone.

              _clearProducts() is intentionally marked as a test helper. Calling it in the running app would erase the whole products collection.

              Checkpoint:

              • Why does updateProduct() return matchedCount instead of the full driver result?
              • How does a route use matchedCount === 0 to choose an HTTP status?
              • Why does connect() accept its URI and database name as arguments?
              4. Map CRUD operations to HTTP routes

              src/product-router.ts maps the database functions to an HTTP API:

              RequestDatabase actionSuccessful response
              GET /productsread all200 and a JSON array
              GET /products/:idread one200 and one JSON document
              POST /productscreate201 and { "id": ... }
              PATCH /products/:idupdate selected fields200
              DELETE /products/:iddelete one200

              A valid-looking ID that is absent from the database produces 404 Not Found. A malformed ID produces 400 Bad Request. These are different failures: the first identifies no document, while the second is not a usable MongoDB ID.

              src/validate-id.ts handles malformed IDs before a route calls the database. Without that middleware, new ObjectId('badID123') throws and looks like a server failure. ObjectId.isValid() lets the app report the client's input error accurately.

              src/app.ts first registers express.json(), then mounts the router at /products. The parser must run first so a POST or PATCH handler can read req.body.

              src/index.ts opens the real database connection before the server listens. Tests import app.ts instead, connect to their own temporary database, and do not run index.ts. This is why the app and server are split into two files.

              Exercise:

              • 1. Trace PATCH /products/abc... from app.ts to the response.
              • 2. Write down each function it passes through.
              • 3. Repeat with a malformed ID and notice where the path stops.
              5. Understand the test setup

              The tests use three tools with different jobs:

              • Vitest runs test files and provides describe, it, expect, and hooks.
              • Supertest sends HTTP requests directly to the Express app.
              • MongoMemoryServer starts a temporary real MongoDB process for the tests.

              The hooks establish a predictable lifecycle:

              text
              beforeAll:  start temporary MongoDB and connect once
              beforeEach: empty the products collection
              test:       arrange data, send request, check response and state
              afterAll:   disconnect and stop temporary MongoDB

              Cleaning before every test prevents one test's documents from changing a later test's result. Stopping MongoDB in afterAll prevents the test process from hanging.

              The main matchers in this project are:

              • toBe() for exact primitive values such as a status code;
              • toEqual() for arrays or objects with the same contents;
              • toMatch() for the 24-character ID pattern;
              • toHaveLength() for array size;
              • toContain() and .not.toContain() for membership.

              The PATCH test checks more than the changed price. It also checks that name and quantity stayed the same. That is what proves the code performs a partial update.

              The DELETE test verifies both the response and the database state by trying to read the deleted product and by checking the final product list.

              6. Suggested study session
              First 20 minutes: vocabulary and data flow
              • Draw the path from an HTTP request to MongoDB and back.
              • Label the app, router, database layer, collection, and response.
              • Explain collection, document, field, and ObjectId aloud.
              Next 25 minutes: direct Mongo practice
              • Work through db-explore.ts one function at a time.
              • Predict what each query matches.
              • Verify the resulting documents in getAllProducts() or MongoDB Compass.
              Next 25 minutes: API code
              • Read db.ts, then product-router.ts, then app.ts, then index.ts.
              • For each route, identify its input, database function, success status, and

              missing-resource behavior.

              Final 20 minutes: tests
              • Run npm test.
              • Temporarily change one expected status to the wrong value and read the

              failure. Restore it afterward.

              • Explain why every hook is beforeAll, beforeEach, or afterAll.
              • Add one extra GET-by-ID success test on your own.
              Final self-check

              You are ready to move on when you can answer these without opening the code:

              • 1. What is the difference between a MongoDB document and a JavaScript object?
              • 2. Why is a string ID converted to an ObjectId?
              • 3. What do $gte and $set do?
              • 4. Why does PATCH use optional fields?
              • 5. Why do malformed and missing IDs have different status codes?
              • 6. Why does each test need a clean collection?
              • 7. Why does the test suite import app instead of index?

              Useful commands:

              bash
              npm run typecheck
              npm run build
              npm test
              npm run explore
              npm run dev
              Week 4 · complete original guide

              Original Markdown snapshot · Classes/si-679-f-26-week-04-nyt-kelvintiger/STUDY_PLAN.md

              Week 4: REST APIs, layers, and tests

              Start with this: a client asks for something, the server does the work, and the server sends an answer. In this project, the thing is a product stored in MongoDB. The client could be Postman, a browser app, or one of our tests.

              The lecture and Now You Try instructions are the source for this exercise. We built the lecture's GET and POST routes, GET by id for Now You Try #1, its tests for #2, and both DELETE stretch items.

              1. What an API request contains

              An API is an agreed way for one program to ask another program to do something. Here the agreement uses HTTP. A request has a method and a path, and sometimes a body. For example:

              text
              GET /products

              GET means read. /products names the collection of products. The server answers with a status code and a JSON body. JSON is text representing objects and arrays. It looks like JavaScript, but keys must have double quotes.

              json
              [
                {
                  "id": "507f1f77bcf86cd799439011",
                  "modelName": "Coffee maker",
                  "modelNumber": "CM1",
                  "manufacturer": "Acme",
                  "color": "Black",
                  "price": 49.99,
                  "quantity": 10
                }
              ]

              The outer square brackets mean an array. Each object is one product. That id is an illustration; use an id from your own response when making a request.

              2. What REST adds

              REST gives us principles for keeping the client/server agreement consistent. Use resource names such as /products and let the method describe the action.

              Method and pathMeaningSuccess code
              GET /productsRead the collection200
              GET /products/:idRead one product200
              POST /productsCreate a product201
              DELETE /products/:idRemove a product204

              :id is a placeholder in our route definition. A real client replaces it with the product's id. GET /products/507f1f77bcf86cd799439011 is a concrete request.

              200 means success with an answer. 201 means created. 204 means success with no response body. 400 means the request is malformed. 404 means the resource or route does not exist. 500 means an unexpected server failure.

              REST also says requests should be stateless: the request supplies what is needed to handle it. The server does not need to remember which product you asked for last time. This does not mean the database cannot store products between requests.

              The client receives a representation of a product: a JSON copy of its current fields. It does not receive direct control of the database document. If it changes its local copy, the stored product does not change.

              The lecture's six constraints are a uniform interface, client/server separation, stateless requests, cacheability, a layered system, and optional code on demand. Our exercise concentrates on naming, HTTP methods, representations, and layers. It does not implement every feature of a complete shop API.

              3. Why split the code into layers?

              We could put a database query inside every route. It works initially, but then database details, HTTP details, and application decisions are mixed together. This week separates those jobs:

              text
              client request
                  -> app -> route -> controller -> service -> database
                                           <- product <- document
              client response <- controller

              The model helps the service turn the document into a product. These are folders inside one server program, not separate servers.

              FileIts job
              src/index.tsConnect to MongoDB, then start listening
              src/app.tsAssemble Express and middleware
              src/routes/product-routes.tsMatch methods/paths to controllers
              src/controllers/product-controllers.tsRead the request and send the HTTP response
              src/services/product-service.tsCoordinate product operations and conversion
              src/models/product.tsDescribe the Product type and convert fields/documents
              src/db/db.tsConnect and run MongoDB operations
              src/middleware/validate-id.tsCheck the path id before using it
              src/middleware/validate-product.tsCheck supplied product fields
              src/middleware/error-handler.tsSend a response for unexpected errors
              src/__tests__/products.test.tsCheck behavior against a temporary database

              The route knows about the controller. The controller knows about the service. The service knows about models and the database. The database does not know about Express. That direction matters: changing the HTTP response should not require changing a Mongo query.

              4. Follow GET for one product

              Open product-routes.ts. This line registers the route:

              ts
              productRouter.get('/:id', validateId, productControllers.getProduct);

              app.ts mounts this router at /products, so the complete path is /products/:id. validateId runs first. A Mongo ObjectId is represented here by 24 hexadecimal characters (digits and letters a–f). An invalid shape receives 400.

              The controller reads req.params.id. params is where Express puts values matched by path placeholders. The controller calls:

              ts
              const product = await productService.get(String(req.params.id));

              The service calls db.getInCollection(db.PRODUCTS, id). The database converts the string to an ObjectId and searches for a document whose _id matches:

              ts
              findOne({ _id: new ObjectId(id) })

              If nothing matches, Mongo returns null. The service returns null too. The controller turns that into 404. The service does not choose 404, because that is an HTTP decision.

              If a document exists, productFromDocument creates a new object containing only the product fields. Mongo's _id becomes a string named id. The controller calls res.json(product), which sends that object with status 200.

              5. Read the TypeScript without getting stuck
              ts
              const get = async (id: string): Promise<Product | null> => {

              Read it as: "get takes a string id, does asynchronous work, and eventually gives back a Product or null."

              • const declares a name that cannot be reassigned.
              • async means the function returns a Promise: an eventual result.
              • await waits for that eventual result before continuing this function.
              • Product | null means either a Product or no matching product.
              • Product[] means an array of Products.
              • Promise<void> means asynchronous work with no useful return value.

              type Product describes an object's fields for TypeScript. It helps the compiler catch mistakes; it does not check incoming JSON at runtime. Middleware does that. import type brings in a type for the compiler. Normal import brings in code that runs. Imports end in .js because that is the extension after compilation.

              Partial<Product> makes every Product field optional. The lecture uses it for incoming fields. fields.price ?? 0 supplies zero when price is missing, while preserving a supplied zero. .map(...) builds a new array by converting each element of the old array.

              6. Follow POST and DELETE

              For POST, express.json() parses the JSON into req.body. Validation checks that it is an object and that any supplied fields have the right types. Strings stay strings; price and quantity must be nonnegative numbers, and quantity must be a whole number. Omitted fields are allowed, matching the lecture's defaults.

              productService.add selects the six known product fields and inserts them. Mongo creates _id; the caller cannot choose it by supplying id or _id. The controller responds with 201 and { "id": "..." }. Use that id in GET.

              For DELETE, Mongo's deleteOne returns a result with deletedCount. The service turns that into a boolean. The controller sends 204 if one product was deleted, or 404 if none was. Deleting twice should produce 204 then 404.

              Middleware order is important: parse JSON, route the request, handle unknown routes, then handle errors. Error middleware has four parameters, with err first; Express uses that signature to recognize it. Express 5 forwards rejected Promises from async controllers to the error handler.

              7. Run it yourself

              From this repository folder:

              bash
              npm ci
              npm test
              npm run typecheck
              npm run build

              Tests do not need your normal MongoDB server. To use Postman or curl, start your normal MongoDB separately at 127.0.0.1:27017. In Compass, connect to it, create database week4 and collection products, and import sampleData/products.json. Then:

              bash
              npm run dev

              Keep that terminal open. In another terminal:

              bash
              curl -i http://localhost:6790/products
              curl -i -X POST http://localhost:6790/products \
                -H 'Content-Type: application/json' \
                -d '{"modelName":"Practice coffee maker","price":25,"quantity":3}'

              Copy the new id from POST. Replace YOUR_ID below with it:

              bash
              curl -i http://localhost:6790/products/YOUR_ID
              curl -i -X DELETE http://localhost:6790/products/YOUR_ID
              curl -i http://localhost:6790/products/YOUR_ID

              Use a product you created for practice, because DELETE removes it. Expect 200, then 204 with an empty body, then 404. Stop the dev server with Control-C. npm run build followed by npm start runs compiled JavaScript instead of the development watcher. Optional MONGO_URI, DB_NAME, and PORT overrides let you point this app at a separate practice database.

              If you get connection refused, MongoDB is not reachable at the chosen address. If the port is in use, stop the other server or choose another PORT. If GET returns [], check the database/collection and your import. A 400 id error means you did not replace YOUR_ID with a real 24-character id.

              8. What the tests teach

              Vitest runs the tests. Supertest sends HTTP requests to our Express app. A MongoMemoryServer launches a real temporary MongoDB on a separate port. The tests call db.init(mongo.getUri(), 'week4-tests') so they use that database.

              beforeAll starts/connects once. beforeEach empties only that test database's products and inserts the same two coffee makers. afterEach restores the one service mock used for the error test. afterAll disconnects and stops MongoDB. Never call _clearCollection on your normal database just to run these tests.

              Each test follows arrange, act, assert. Setup arranges two known products; a request acts; expect checks the result. GET's setup inserts directly through the database layer so a broken POST does not cause a misleading GET failure.

              Use toBe for values like a status number, toEqual for an object's contents, toHaveLength for array size, and toMatch for an id's shape. Two separate objects can have equal contents while failing toBe, which checks identity.

              The suite checks list shape, creation/readback, successful and absent GET, successful and absent DELETE, empty collections, defaults, invalid input, unknown paths, and unexpected service errors. It runs the source tests once; compiled copies under dist/ are excluded.

              Supertest imports app.ts, so it does not execute index.ts. Passing tests alone do not prove the real server starts. That is why checking npm start and actual HTTP requests is a separate step. GitHub's small classroom check also does not substitute for these route checks or the instructor's review.

              9. Practice and check yourself
              • 1. Trace GET by id through the five layers without running it. At which point

              does the id become an ObjectId? At which point does it become a string again?

              • 2. Create a product with price zero. Explain why ?? preserves that value.
              • 3. Write a test that creates and deletes a product, then confirms GET returns 404.
              • 4. Think through adding PATCH without coding it yet: which layer chooses the

              response status, which layer coordinates the update, and which talks to Mongo?

              • 5. Change a test's expected status to the wrong number, run that test, and read

              the failure. Restore the expectation afterward.

              Answers: ObjectId conversion happens in the database query; document-to-product conversion makes the response id a string. ?? only replaces null/undefined, not zero. The controller chooses HTTP status, the service coordinates the update, and the database layer executes it. A good deletion test checks both the delete response and a later read, rather than trusting the first response alone.

              Suggested first session: spend ten minutes on requests/statuses, fifteen tracing GET through the files, fifteen making POST/GET/DELETE requests, and ten reading and changing one test. If you can explain why a missing product becomes null in the service but 404 in the controller, you understand the central idea of today.

              Code folders and reading order

              FolderRead in this order
              week1hello / Week 1 NYTserver.js → corresponding request in Postman/curl.
              TypeScript basicsREADME.md → src/index.js → src/index.ts → expected demo output.
              Week 2types.ts → books-service.ts → books-router.ts → app.ts → index.ts → tests.
              HW1types/data → auth/validation → routers → app/index → auth/menu/orders/errors tests.
              Week 3db-explore.ts → db.ts → validate-id.ts/product-router.ts → app/index → tests.
              Week 4db/db.ts → models/product.ts → services → controllers → routes/middleware → app/index → tests.

              Week 3 · HTML source · Week 4 · HTML source · Week 3 prep · HTML source · Week 4 memo · HTML source · Week 5 · HTML source · Week 6 · HTML source

              Source trail · Current course pages, local files, and retained guides. Original guide prose is preserved.

              ↑ See glossary