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.
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
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.
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.
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
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.
Notice that a failed check replies before the order reaches the service or the database.
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.
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 valuetypes 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
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?
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
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.
0+
Before Week 1 · terminal, Node, an editor, git, and a first server
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
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
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.
Download the LTS installer from nodejs.org ↗ and run it. LTS means long-term support: the release meant for everyday use.
Close the terminal and open a new one, so it can find the new programs.
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.
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
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.
Open VS Code.
Choose File, then Open Folder.
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 codecommand. 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
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.
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.
Open the assignment link and accept it. GitHub creates a repository for you.
On that repository’s page, press the Code button and copy the URL.
Clone it, move into the new folder, and install its packages. Paste your own URL in place of the example one.
Do the work in your editor and save the files.
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
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 HTTPrequests. 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.
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
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.
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
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.
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
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.
Run mongosh --version. If the command is not found, install the shell separately: mongosh instructions ↗. Some platforms do not include it with the server.
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.
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.
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.
Open Compass and click Add new connection.
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.
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.
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
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.
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
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.
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.
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);
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.
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.
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.
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);
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.
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.
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.
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);
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.
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.
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');
}
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);
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.
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.
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.
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 forloop.
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.
$ 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.
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.
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.
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}`);
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.
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);
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;
};
Python’s async def and await work the same way, but only Python warns about a coroutine that was never awaited.
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 namefield, 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 Solved
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.
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.
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 customerfield.
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')
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.
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.
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.
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.
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.
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 Solved
Name the parts of one request
Put each name where it belongs on the map.
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 Supertesttest 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.
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
POST /tracks HTTP/1.1The start line. The method is POST, the path is /tracks, and HTTP/1.1 names the protocol.
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.
Content-Type: application/jsonAnother header. Headers describe the request; this one says the body is JSON.
{"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);
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
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.
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.
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
app.use(express.urlencoded({ extended: true }));Runs before the routes on every request and parses an HTML form body into req.body.
app.use(express.json());The same job for a body written as JSON.
app.post('/submitform', (req, res) => {Registers a route: when a POST request for /submitform arrives, Express runs this function.
const { address, city, zipcode } = req.body;Destructuring: three names from three body fields, each matching a form input’s name.
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.
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
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.
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.
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.
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.
return;Stops the callback after the error response, preventing a second response.
res.send(`You asked for item ${itemid}`);Reached only with valid input. A template literal uses backticks and ${...} to insert values.
app.listen(3000);Starts the server. It now waits for requests on port 3000.
Rebuild it · put the lines in order Solved
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
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
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.
Render a first/last-name form, then destructure fname and lname from the body.
GET /people?letter=a
Use 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.
Validate the query as nonblank numeric text; return the requested item sentence, or 400.
GET /play/artist/:artist/song/:song
Insert both decoded path values into a listening sentence.
GET /getform
Return an address/city/ZIP form targeting POST /submitform.
POST /submitform
Use form fields to describe the shipping destination.
GET /browse/:category?keywords=coffee
Combine one path parameter and one query input.
GET /hithere?name=Ada
Return a greeting using the query value.
POST /tracks
Require nonblank string artist and title; return status, trackAdded, and an ISO timestamp. Local success is 200.
GET /products/:department/:category?keywords=lcd,smart,sony
Use 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
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
mkdir week1helloMakes a new folder for the project.
cd week1helloMoves the terminal into that folder, so the commands below act on it.
npm init -yCreates package.json, which describes the project and scripts.
npm install expressAdds Express as a dependency. Dependencies go under node_modules.
npm pkg set type="module"Writes "type":"module" into package.json, which selects ES-module import syntax.
# 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.
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
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.
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 argumenttype, 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.
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 Solved
What runs, and what does not
Put each name where it belongs on the map.
This exercise needs JavaScript. The map above shows every name in its place.
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
enum BookStatus {An enum gives names to allowed status values. This line and the three below it list every status a book can have.
interface Book {The words interface Book describe the fields of the object. Nothing is created; values are checked against it.
year: number;One field: its name, a colon, then its type.
status: BookStatus;A field can use a type you declared. Only the three enum values fit here.
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.
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.
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.
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
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.
type BookFilter = string | number;The typestring | number is a union: one of those possibilities. The word type gives the union a name.
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.
if (typeof filter === "number") {Checking with typeof narrows the union. Inside this branch TypeScript knows the filter is a number.
return catalog.filter(book => book.year === filter);So it can be compared with year, which is a number.
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.
const maybeBook = findBookByTitle(catalog, "Dune");The result is a Book or undefined, as the signature of that function says.
if (maybeBook !== undefined) {Narrowing again. Inside the braces the value is a Book, so reading its title is allowed.
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']
A failed .find() returns undefined. A failed MongofindOne() 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
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
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.
return items[0];The first element. An empty array has none, which explains the undefined possibility.
const firstNumber = getFirstItem([1, 2, 3]);TypeScript infers Item as number from the argument, so the result is number or undefined.
const firstBook = getFirstItem(catalog);The same function, with Item now inferred as Book.
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
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.
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
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 Nodeserver.
With these projects’ ES-module/NodeNext setup, local TypeScriptimports 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
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
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.
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:
Object-literal colons stay. In { id: user.id } the colon pairs a field with its value. That is JavaScript, not a type.
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:
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
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.
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.
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.
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.
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:
.map(change) turns every item into something new. Week 6’s repository does docs.map((doc) => toProduct(doc)), so every MongoDBdocument 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.
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.
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.
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
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.
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.
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.
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 Authorizationheader, 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
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.
Go to the file and line first. Do not read the whole message yet.
Read the message. It usually says X is not Y. Find X on that line.
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:
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 Authorizationheader, 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.
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
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.
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
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);
});
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
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:
Copy the whole repo, so your real one is never touched.
Save the file you are about to blank somewhere outside the repo.
Blank it, but keep its import lines. They tell you what the file uses.
Rewrite the rest from memory, without looking.
Run tsc, then the tests. Fix the first error first.
Diff your version against the original and read every difference.
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
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.
03
Week 2 · routers, middleware, services, and first tests
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 Solved
Where middleware sits
Put each name where it belongs on the map.
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
app.use('/books', booksRouter);In app.ts, the app supplies a mount prefix. Every request whose path starts with /books goes to the router.
const booksRouter = express.Router();In books-router.ts. A Router collects related handlers.
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.
// A client sends GET /books/123, not GET /:id.A client always sends the full path with a real id.
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 serviceexports 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
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.
const logger = (req, res, next) => {Ordinary middleware takes (req, res, next): the request, the response, and a function that passes control on.
console.log(req.method, req.path);The work this middleware does: print the method and the path.
next();Calling next() hands control to the next applicable handler. Sending a response finishes the request. Doing neither leaves it waiting.
booksRouter.use(express.json());Router-wide middleware applies to matching router traffic. This one parses a JSONbody, and it is registered first, so it runs first.
booksRouter.use(logger);Also router-wide; registered second, it runs after the parser.
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 Solved
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.
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
Flask moves on when a before_request function returns None; Express moves on only when you call next().
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.
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
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
Route
Protection and input
Behavior in your local code
POST /books
Auth + title/author validation; body title, author, year
201 with the new book. id is Date.now(); default status available.
GET /books
Public; optional title, author, year queries
200 with an array; supplied filters are exact matches applied together. Year converts from URL text to a number.
Throw 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 runtimeobject.
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
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
app.use('/books', booksRouter);Routes are registered first.
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.
res.status(500).send('An unexpected error occurred');Answers every error it receives with 500 and a short message.
export { app };Offers the app to other files. This file never starts listening.
app.listen(6790);In index.ts, the only file that starts the real listener, on port 6790.
Rebuild it · put the lines in order Solved
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.
// 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.
Testsimport 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.
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
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 parsedJSON 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
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
Install Express as a runtimedependency; 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 block
Every requested task
NYT #1 · filtering
Implement exact title, author, and year filters with AND when combined. Create corresponding Postman requests.
Change 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 · tests
POST 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
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
The path starts with /books, so the request enters the books router.
express.json() finds no body to parse and continues.
logger prints the method and path, then continues.
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.
1 · The request as HTTP text
A start line, the headers, a blank line, then the body.
HTTP request
2 · What Express reads from it
The req object
3 · Each step in order
Each line is one middleware or route, labelled with whether it ran. Exactly one line sent the response.
4 · The response
HTTP 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.
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 port3000. It combines Week 1 inputs with Week 2 routers, middleware, and tests. It has no MongoDBpersistence. Menu data is supplied; stored orders get server-created id, status, and timestamp.
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 Solved
SliceDrop on the map
Put each name where it belongs on the map.
This exercise needs JavaScript. The map above shows every name in its place.
Endpoint
Who can call it
Expected answer
GET /menu
Public
200; all six items, or exact category filter.
GET /menu/:id
Public
200 with one item; 404 with an error for an unknown id.
POST /orders
Customer token
201 with created order; 400 with errors for an invalid order.
GET /orders
Staff token
200 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.
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 /ordersThe method and the path: create an order.
Authorization: Bearer slicedrop-customer-secretA header carrying the customer token, after the word Bearer and one space.
Content-Type: application/jsonSays the body below is JSON.
"customerName": "Ada",The customer name. The body carries no id, status, or timestamp; the server creates those.
"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
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
export function parseBearerToken(header: string | undefined): string | null {Takes the Authorization header, which may be missing, and returns the token text or null.
if (header === undefined) return null;No header was sent, so there is no token to read.
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.
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];
}
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 fieldvalidation.
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
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
If supplied, array whose every entry is an offered string
A 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 runtimevalues 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
"customerName": " ",Only spaces. After trim() nothing is left, so the validator adds an error.
"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.
// With the customer token: 400 and multiple validation errors.The handler turns the nonempty errors array into one 400 response that lists every error.
// 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
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-memoryarray. Unfiltered listing preserves insertion order; status filtering keeps exact matches. Restarting Node clears the orders and resets the counter.
500; {"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
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 / group
Study job
app.ts / index.ts
Parser, mount points, fallback, errors; separate listener.
Accumulate field errors; call offered-option helpers safely.
data/menu.ts / data/orders.ts
Supplied menu lookup; process-local order state and test reset.
types.ts / constants.ts
Shared request/stored shapes; exercise tokens and port.
auth.test.ts
Missing/bad/wrong-role tokens; authorized cases; auth before validation.
menu.test.ts
Six items, category filters, customization fields, and missing id.
orders.test.ts
201, unique ids, pending status, parseable time, list/filter/insertion order.
errors.test.ts
404, 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
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.
05
Week 3 · MongoDB, BSON, CRUD, ids, and Express integration
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.
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.
MongoDB
Rough relational comparison
Example
Database
Database
week3db, week3app, week4
Collection
Table
products
Document
Row
One tape or coffee-maker record
Field
Column
name, 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 mongodbnpmpackage 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
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
import { MongoClient, ObjectId } from 'mongodb';Loads two tools from the mongodb package. The line below it loads two types, which only TypeScript uses.
const client = new MongoClient('mongodb://127.0.0.1:27017');Creates a client for the MongoDBserver on your own computer, port 27017.
await client.connect();The first of three distinct steps: connect to the server. The await pauses here until the connection is ready.
const db = client.db('week3db');The second step: select a database by name.
await db.command({ ping: 1 });Checks the connection: it sends the server a ping and waits for the answer.
const products = db.collection('products');The third step: select a collection. The operations below run on it.
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
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.
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.
const all = await products.find().toArray();Read everything. Because find() returns a cursor, toArray() is what gives the array.
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.
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.
{ _id: inserted.insertedId },An update takes two objects. The first is the query: the document with this id.
{ $set: { quantity: 150 } }The second says what to change. Here $set changes quantity and preserves the other fields.
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 Solved
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.
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.
replaceOne 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.
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
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
const id = '507f1f77bcf86cd799439011'; // illustrative shapeThe id as the URL carries it: a string of 24 hexadecimal characters.
const document = await db.collection('products').findOne({Asks the products collection for one document.
_id: new ObjectId(id)A query against an ObjectId-valued _id needs new ObjectId(id), because an ordinary string has a different BSONtype.
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) => {A middleware function: it receives the request, the response, and next.
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.
res.status(400).json({ error: 'Malformed id' });Answers 400 here. No ObjectId is constructed and Mongo is never asked.
return;Stops, so next() is not called after the response.
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 Solved
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.
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
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 requestfields and HTTPstatuses. app.tsparsesJSON and mounts /products. index.ts awaits connection before listening on 6790.
productRouter.get('/:id', async (req: Request, res: Response) => {Registers GET for one product. The handler is async because it waits for the database.
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.
if (!product) {When no document has that id, getProduct gives null, and null counts as false.
res.sendStatus(404);The router also owns HTTP statuses: absent means 404. The return on the next line stops the handler.
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 runtimebodyvalidation.
_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 Solved
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
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
Activity
What you must be able to write and verify
NYT #1 · findPricierTest
Find all products with price ≥ 5 using $gte.
NYT #1 · updatePriceTest
Set Scotch Tape’s price to 4.29 with updateOne and $set.
NYT #1 · deleteByNameTest
Delete Masking Tape by name with deleteOne.
NYT #1 stretch · markOnSaleTest
Use updateMany and $lt:5 to add onSale:true to the matching products; inspect differing documentfields in Compass.
Reject 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.”
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
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.
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.
The query object has one condition: price must be greater than or equal to 5.
Three products meet it. Packing Tape counts, because $gte includes a price of exactly 5.
find() gives a cursor, and toArray() reads it into the array below.
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.
1 · The driver call
This is how the operation reads in a TypeScript file.
TypeScript
2 · What comes back
3 · Why
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.
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.”
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 Solved
The layers behind the contract
Put each name where it belongs on the map.
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.
Constraint
What it means
In this course’s API
Uniform interface
Clients and servers talk in one consistent way: standard HTTPmethods 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–server
The 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.
Stateless
Each 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.
Cacheable
A 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 system
The 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 · optional
A 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
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 fieldsclients see. Then lay out plural resource nouns, using HTTPmethods for actions. The database’s storage arrangement should not dictate every public path.
Representation in the lecture scenario
Fields and relationship
Customer
id, firstName, lastName, zipCode. zipCode is text, preserving leading zeros.
id, customerId, status, items keyed by product id with quantities.
Relationships
A customer has orders; each order refers to a customer and contains quantities of products.
Endpoint design
Operations
/customers
GET collection; POST create.
/customers/:id
GET one; PATCH supplied fields; DELETE one.
/products
GET collection; POST create.
/products/:id
GET one; PATCH supplied fields; DELETE one.
/customers/:id/orders
GET 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
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
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.
Assemble 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
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
productRouter.get('/:id', validateId, productControllers.getProduct);Route. Id validation stops bad shapes before Mongo, then the controller runs.
const product = await productService.get(String(req.params.id));Controller. It reads the path id and asks the service, never Mongo.
if (!product) {The service passed back null, so no product has this id.
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.
res.json(product);Found, so send the product.
const doc = await db.getInCollection(db.PRODUCTS, id);Service. It asks the databaselayer for the document.
return doc ? productFromDocument(doc) : null;The service converts an existing document to Product or passes back null. It chooses no status.
.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 Solved
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.
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.
id: productDocument._id.toString(),The line that matters. The _id, an ObjectId, becomes id, a string. Clients get a string id and no _id.
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
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.
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 objectin 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 bodymiddleware rejects arrays/non-objects and wrong supplied field types; price/quantity must be finite nonnegative numbers and quantity an integer. Omitted fields remain allowed.
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
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
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
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.
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
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.
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 objectin 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
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.
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 Solved
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.
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 fix
Every field, plus a placeholder id beside _id.
After both fixes
Every 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
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
Task
Required understanding / evidence
Lecture · GET /products
Import products.json into week4/products; build all layers; receive two coffee makers with public id fields.
Lecture · POST /products
Send raw JSON in Postman; receive 201/{id}; list grows to three; Compass confirms the stored document.
NYT #1 · GET /products/:id
Add 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 · DELETE
Carry delete through the layers; choose a success response; list gets shorter and Compass agrees.
Exactly 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 tests
describe 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 test
Post 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 tests
Delete 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
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.
Importing app does not execute index.ts startup or test a real fixed-port server.
MongoMemoryServer
Launch 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
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
let mongo: MongoMemoryServer;Declared outside the hooks, so beforeAll can fill it and afterAll can use it.
beforeAll(async () => {Runs once, before the first test.
mongo = await MongoMemoryServer.create();Starts a temporary MongoDB. This is the expensive step, which is why it happens once.
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.
beforeEach(async () => {Runs before every test.
await db._clearCollection(db.PRODUCTS);Empties the products collection, so nothing is left from the test before.
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.
vi.restoreAllMocks(); // when a test temporarily replaced behaviorAfter every test, restores any behavior a test replaced with a mock.
afterAll(async () => {Runs once, after the last test: disconnect, then stop the temporary MongoDB.
Rebuild it · put the lines in order Solved
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.
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.
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
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.
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 servicefunction directly and a leftover key set to undefined, such as _id, should fail; a bodyparsed 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.
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
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
describe('GET /products/:id', () => {Groups the tests for one endpoint under a name.
it('returns the requested product', async () => {One test. The text says what should be true, and the function checks it.
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.
expect(list.status).toBe(200);An assertion: the status must be 200.
expect(list.body).toHaveLength(2);The list holds the two seeds and nothing else.
const expected = list.body[0];Keeps the first product, so the test uses a real id from this run.
const res = await request(app).get(`/products/${expected.id}`);Act: ask for that one product by its id.
expect(res.body).toEqual(expected);The product returned alone must have the same contents as the one in the list.
expect(res.body._id).toBeUndefined();The public product must not expose _id.
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 Solved
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’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
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 Mongoserver, 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.
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
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.
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 MongoDBdocument 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.
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 Solved
What a unit test replaces
Put each name where it belongs on the map.
This exercise needs JavaScript. The map above shows every name in its place.
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 see
It 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, $set
The repository. MongoDB details stay in one layer.
app.ts and index.ts are two files for the tests’ sake. app.ts builds the app and exports it; index.tsimports 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
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.
Notice that all three answers start at the service: what it returns or throws decides whether the client gets 200, 404 or 400.
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:
Find: ask the repository for the stored product. None means return null and stop.
Compute: work out the quantity and price the product would end up with.
Validate: a negative result throws a ValidationError.
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:
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.
Every 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
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.
Anything unexpected, such as the database being down
Lets the error pass through
Error handler
500 { 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
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.
Supertest sends a PATCH, then a GET reads the product back
Regression
Any of the above
Depends on the test
Running 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
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.
Starts 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
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);
});
Arrange: create the conditions. The test reads the list and takes the first product’s id. It trusts GET here; other tests check GET.
Act: do the one thing under test. Send the PATCH and keep the response.
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:
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.
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
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
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.
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
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.
__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
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.
Notice the two powers: the test decides what the mock answers, and afterwards it asks the mock how it was called.
Word
What it is
In Vitest
Fake
A stand-in function that does nothing but remember every call
A 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 double
The 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
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
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);
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
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
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
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.
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
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
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
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
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
Spied 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']
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
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();
});
Import
Why
productsService
The functions under test. The file name says so: it sits next to products-service.ts.
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
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 Solved
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
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.
Evidence
Asks
Matcher
The result
What did the function hand back or throw?
rejects.toThrow(ValidationError), toBeNull()
The side effect
Did 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
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.
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
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:
Notice that each row does everything the row above it does, and one thing more.
Method
Call history
Answers you configured
Original method
mockClear()
Cleared
Kept
Still replaced
mockReset()
Cleared
Cleared, back to how the fake started
Still replaced
mockRestore()
Cleared
Cleared
Put back, for a spy
TypeScript · composed for this guide, run with Vitest 3.2
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
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.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');
});
Piece
Its 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
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:
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
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.
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
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.
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 HW1
In HW2
Where the course taught it
Orders live in an arrayin 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.
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
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.
Task
Layer
Where the course taught it
Status codes
Describe the new menu: a dessert category and crusts that have prices
None. 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
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.
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).
// 1. is the username free?
const existing = await usersRepository.findUserByUsername(username);
if (existing) {
throw new ConflictError('That username is taken');
}
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.
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).
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.
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.
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
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
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.
The async change. Make the order validator async. The rules do not change. Typecheck should now pass.
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.
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.
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 graded
Points
The menu suite
25
The orders suite
25
The auth suite
25
The errors suite
25
npm run typecheck passes
20
Total
120
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
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.
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 stringid, 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
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.
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.
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 Solved
Where each auth check runs
Put each name where it belongs on the map.
This exercise needs JavaScript. The map above shows every name in its place.
The token is valid, but the role is wrong or the caller is not the owner
403
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.
By the end of the week the API has five guarded or new routes:
Route
Guard
Answers
POST /auth/register
None
201 with the public user; 400 if a field is missing; 409 if the username is taken
POST /auth/login
None
200 with a token; 401 for a wrong username or password
GET /auth/me
A valid token
200 with the token’s payload; 401 otherwise
POST /products
A valid token with the staff role
201 for staff; 401 with no token or a bad one; 403 for a customer
GET /users/:id
A valid token, then an ownership check in the service
Staff 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
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.
Input
Output
apple
5
banana
6
mango
5
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 adds
What it does
What it defeats
A salt
A random value mixed in before hashing, so the same password hashes differently for every user
A precomputed table of the hashes of common passwords, called a rainbow table
A cost
It is slow on purpose: cost 11 means 2048 rounds of work
Millions 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
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;
};
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.
The repository does what the products one does. Its toUser function turns a MongoDBdocument into a User, so _id never leaves the databaselayer. 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
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 };
};
Situation
The service
Response
The username or the password is missing
Throws a ValidationError
400
The username is already taken
Throws a ConflictError
409
Otherwise
Hashes the password, inserts a customer, returns a PublicUser
201
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
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.
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
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 has
Status
Body
The right username and password
200
{ token }
A username nobody has
401
Wrong username or password
The right username, the wrong password
401
Wrong username or password
Why hand back anything? HTTPrequests 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
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
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.
Computed 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
Claim
Meaning
sub
Subject: whose token this is. Here, the user’s id
iat
Issued at: when the token was signed
exp
Expires: when the token stops being accepted
username, role
Claims 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.
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
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.
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.
// 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
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();
};
Read the Authorizationheader. If it is missing or does not start with Bearer , throw an AuthenticationError.
Slice the prefix off. What is left is the token.
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.
On success verify returns the payload. Store it in res.locals.user, the place Express provides for middleware to leave data for later handlers.
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
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');
});
Re-encode a changed payload and keep the original signature
401
From a real login
Register, log in, and use the token that comes back
200 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
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 Solved
Put the guards in order
Rebuild the route that lets only staff create a product.
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.
Notice which gate answers: 401 comes from requireJwt and 403 from requireRole, and the controller never runs for either.
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.
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
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.
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
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
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.
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');
}
Version
End-to-end 403 test
Unit test
What a customer can learn
Check, then look up
Passes
Passes
Nothing: every other id answers 403
Look up, then check
Passes
Fails: findUser was called
Which 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 that
Use
The service looked up exactly the id that was asked for
toHaveBeenCalledWith('grace-id')
A user that does not exist gives null
mockResolvedValue(null)
The hashed password never leaves the service
not.toHaveProperty('hashedPassword')
A refused caller caused no lookup
not.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
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.
A stolen token works until its exp time; there is no server-side logout
The role is copied into the token at login
A user who is demoted keeps the old role until the token expires
No refresh
After one hour the user has to log in again
No reset, no email check, no second factor
A forgotten or guessed password has no remedy
Two roles only
There 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
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
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.
08
Practice lab · trace, predict, write, and check
Try it before revealing it.
Choose a study session
Time available
A useful sequence
30 minutes
10 min HTTP inputs/statuses → 10 min layers/ObjectId → 10 min answer recall questions without notes.
90 minutes
15 min Chapters 1–2 → 15 min middleware/HW1 → 20 min Mongo CRUD → 20 min REST trace → 20 min tests/practice.
Several sessions
One 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.
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.
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.
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
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
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.
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
Answer
Read the lesson again:
With the answer showing and the card in focus, press 1 for Got it or 2 for Not yet.
Remove the review schedule for every card? Check results and task ticks stay as they 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.
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.
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.
Project
Scripts to know
Normal app / database
Classes/week1hello
npm start
Port 1679; no database.
Classes/si-679-f-26-week1-nyt-kelvintiger
npm start
Port 3000; no database. Its npm test is a placeholder, not a local test suite.
Classes/si-679-f-26-ts-basics-kelvintiger
typecheck, build, start, dev, js
Console catalog; no HTTP server; no npm test script.
Port 6790; week4/products; sampleData/products.json.
Command
Purpose
npm ci
Install the lockfile’s dependency set for an existing checkout with a valid lockfile.
npm install
Install/update dependencies; used by the lecture starters.
npm test
Usually vitest run, once, in the repositories defining it.
npm run test:watch
Rerun tests as source changes.
npm run typecheck
tsc --noEmit checks types without generating files.
npm run build
Compile TypeScript into dist. HW1 uses a build config excluding tests.
npm run dev
Run TypeScript with tsx (usually watch); does not replace a typecheck.
npm start
Runs 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
Code
Meaning
Course example
200
Successful response
GET/list; Week 1 tracks; Week 3 PATCH/DELETE.
201
Created a resource
Week 2 books, HW1 orders, Weeks 3–4 products POST.
204
Success with no body
Local Week 4 DELETE.
400
Unusable client input
Bad JSON/fields or malformed id.
401
Authentication credential not accepted
HW1 missing/bad/wrong-role token. General semantics differ from the exercise’s simplified role handling.
403
Forbidden
General lack of permission; Week 2 uses it for all failed classroom token checks.
Replace the placeholder with a real returned 24-hex id.
Headers already sent
Control flow
Look for continuing after a response or calling next after responding.
Request hangs
Middleware/await/connection
Did every path call next or answer? Did the database operation finish?
Test passes once but fails later
Shared state
Reset between tests; avoid persistent database leftovers; restore mocks.
Type errors despite running dev server
Compiler vs runner
tsx execution is separate from npm run typecheck.
Port already in use
Another running app
Stop 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.
Term
Meaning
API
The 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
When the types and source of a program are checked, before it runs. Taught in What TypeScript changes
Contract
The agreed input/output behavior that clients and tests rely on. Taught in The exact Week 2 contract
Controller
The 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
A TypeScript type that gives names to a fixed set of allowed values. Taught in What TypeScript changes
Environment variable
A 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 handler
Express 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
A fixed-size fingerprint computed from an input, which cannot be turned back into that input. Taught in Never store the password
Header
A named line of extra information on a request or response, such as Content-Type. Taught in Start with the conversation
Hook
A 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
Code 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
Checking 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
The 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
The 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
Spy
A 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
Each request supplies the context needed to understand it; the server does not depend on remembered conversation. Taught in REST is an architectural style
A 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
JavaScript with type annotations that a compiler checks before the program runs. Taught in What TypeScript changes
Uniform interface
One 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
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.
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.
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.
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:
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:
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
Route
Input source
Main transformation
GET /catalog
query itemid
validate number and place it in a sentence
GET /play/artist/:artist/song/:song
two route parameters
place both decoded values in a sentence
GET /getform
none
return an address form
POST /submitform
form body
combine address, city, and ZIP code
GET /browse/:category
route parameter + query
combine category and keywords
GET /hithere
query name
create a greeting
POST /tracks
JSON body
validate and return a structured JSON result
GET /products/:department/:category
two route parameters + query
turn 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.
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:
Function
Job
addBook
add one book
getAllBooks
return the catalog
getBook
find a book by numeric ID
updateBook
apply selected changes to one book
removeBook
remove the matching ID
_resetCatalog
empty 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:
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
Request
Auth?
Result
POST /books
yes
validate, create a book, return 201 and JSON
GET /books
no
return all books or exact-match filters
GET /books/:id
no
return one book or 404
PATCH /books
yes
update selected fields, 400 for bad input, 404 if absent
DELETE /books
yes
remove the ID and return 200
GET /books/badroute
no
throw 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?
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 path
Who may call it
Behavior
GET /menu
anyone
Return all six menu items, or filter by category.
GET /menu/:id
anyone
Return one menu item by its exact ID, or 404.
POST /orders
customer token
Validate and create an order, or return validation errors.
GET /orders
staff token
Return 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:
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:
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.
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 value
Result
missing/undefined
null
Bearer slicedrop-customer-secret
token string
bearer slicedrop-customer-secret
null because Bearer is case-sensitive
Basic slicedrop-customer-secret
null
Bearer
null because the token is empty
Bearer one two
null 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.
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
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.
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:
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:
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 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:
MongoDB
Rough SQL comparison
Example in this project
database
database
week3app or the test database
collection
table
products
document
row
one Duct Tape product
field
column
name, 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.
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:
Function
Input
Output
connect
URI and database name
an open connection
getAllProducts
none
every product document
getProduct
string ID
one document or null
addProduct
a Product
the new ObjectId
updateProduct
ID and partial changes
number of matched documents
deleteProduct
ID
number 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:
Request
Database action
Successful response
GET /products
read all
200 and a JSON array
GET /products/:id
read one
200 and one JSON document
POST /products
create
201 and { "id": ... }
PATCH /products/:id
update selected fields
200
DELETE /products/:id
delete one
200
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
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.
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 path
Meaning
Success code
GET /products
Read the collection
200
GET /products/:id
Read one product
200
POST /products
Create a product
201
DELETE /products/:id
Remove a product
204
: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:
The model helps the service turn the document into a product. These are folders inside one server program, not separate servers.
File
Its job
src/index.ts
Connect to MongoDB, then start listening
src/app.ts
Assemble Express and middleware
src/routes/product-routes.ts
Match methods/paths to controllers
src/controllers/product-controllers.ts
Read the request and send the HTTP response
src/services/product-service.ts
Coordinate product operations and conversion
src/models/product.ts
Describe the Product type and convert fields/documents
src/db/db.ts
Connect and run MongoDB operations
src/middleware/validate-id.ts
Check the path id before using it
src/middleware/validate-product.ts
Check supplied product fields
src/middleware/error-handler.ts
Send a response for unexpected errors
src/__tests__/products.test.ts
Check 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:
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:
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.
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:
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
Folder
Read in this order
week1hello / Week 1 NYT
server.js → corresponding request in Postman/curl.