From your first Express route to a tested Mongo-backed API. Read the complete lessons, trace a request, then close the notes and explain it. A study companion made to share.
No topics match. Try a shorter term, or clear the search.
01
Week 1 · HTTP, Node, Express, forms, and JSON
A request becomes a response.
Start with the conversation
A client asks for something. A server runs code and sends an answer. A browser, Postman, curl, and a Supertest test can all act as clients. An API is the agreement about which requests are allowed and what responses mean.
Node runs JavaScript outside the browser. Express is a library that helps a Node program receive HTTP requests, match routes, parse data, and send responses. Installing Express does not start a server; your program must create an app and call listen().
Client→HTTP request→Express handler→HTTP response
HTTP request
POST /tracks HTTP/1.1
Host: localhost:3000
Content-Type: application/json
{"artist":"Radiohead","title":"Creep","year":1992}
The method is POST, the path is /tracks, headers describe the request, and the body carries the submitted track. The response also has headers, a status, and usually a body. A URL includes protocol, host, optional port, path, and optional query. localhost means your computer; a port chooses the listening program.
Part
Example
Meaning
Method
GET, POST
Read data or submit data; method + path determine the route.
Path
/tracks
Which endpoint the client is asking for.
Query
?author=Tolkien&year=1937
Named URL inputs, often filters. Separate pairs with &.
Headers
Content-Type: application/json
Metadata; describes the submitted body format.
Body
{"title":"Dune"}
Submitted content; common with POST and PATCH.
Response status
201 Created
What happened, independent of the visible body.
Know where the input lives
Source
Request example
Read it from
Path parameter
/play/artist/Radiohead/song/Creep
req.params.artist and req.params.song
Query parameter
/catalog?itemid=222
req.query.itemid
Form body
A form with name="zipcode"
req.body.zipcode, after URL-encoded parsing
JSON body
POST /tracks with JSON
req.body.artist, after JSON parsing
Header
Authorization: Bearer SI679
req.headers.authorization
A colon in /person/:id defines a placeholder. The client uses a real path such as /person/42; it does not literally send :id. URL encoding represents spaces as %20, which Express decodes. Route parameters are text; query values also need checking before you treat them as a single string or number.
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}`);
});
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. A parsed JavaScript object is no longer a JSON string. JSON.parse() converts text to a value; JSON.stringify() converts a value to text.
Read a route from left to right
JavaScript · simplified from Week 1
import express from 'express';
const app = express();
app.use(express.json());
app.get('/catalog', (req, res) => {
const { itemid } = req.query;
if (typeof itemid !== 'string' || itemid.trim() === '' ||
Number.isNaN(Number(itemid))) {
res.status(400).send('Error: itemid must be a number');
return;
}
res.send(`You asked for item ${itemid}`);
});
app.listen(3000);
app.get registers a route; its callback runs when a matching request arrives. req holds the request and res builds the response. res.send() can send text or HTML, res.json() serializes a value to JSON, and res.status(400) sets a status before sending. res.sendStatus(404) sets a status and sends its text label. status() alone does not finish a response.
Destructuring, as in const { itemid } = req.query, gives a local name to an object property. A template literal uses backticks and ${...} to insert values. Validation happens before using the input. The return after an error response stops the callback, preventing a second response.
Every Week 1 activity, in one map
The week1hello code-along listens on 1679. The Week 1 Now You Try listens on 3000. Similar concepts do not make their paths interchangeable.
Code-along in week1hello
What to understand
GET /, GET /about
Simple text responses and route registration.
GET /contact → POST /submit
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.
GET /league/:leaguename/team/:teamname
Two named path parameters in one route.
GET /person/:id
Convert/check numeric input; reject unusable input with 400.
POST /postData
Read firstName and lastName from parsed JSON.
Week 1 Now You Try route
Input and result
GET /catalog?itemid=222
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.
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
npm init creates package.json, which describes the project and scripts. npm install adds dependencies under node_modules. "type":"module" selects ES-module import syntax. The start script uses Node’s watch mode so saved code restarts the process; an in-memory array is lost during that restart.
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.
02
TypeScript basics · the book catalog exercise
Types describe your assumptions.
What TypeScript changes
TypeScript adds compile-time checking to JavaScript. It helps catch a misspelled field, the wrong argument type, or a possibly missing value before running the program. The compiler emits JavaScript, which Node executes. Type annotations disappear at runtime; they do not validate a request from a client.
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);
}
interface Book describes the object’s fields. Book[] is an array of books. An enum gives names to allowed status values. The function’s signature describes two inputs and a result that can be either a Book or undefined. const prevents reassigning the variable, but you can still push into its array or change an object’s fields.
string | number is a union: one of those possibilities. A typeof check narrows it to the appropriate branch. ?. 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.
A failed .find() returns undefined. A failed Mongo findOne() returns null. Both represent absence here, but they are distinct JavaScript values and their declared types should match their actual behavior.
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
Item is a type parameter, chosen or inferred for each call. The input array’s element type becomes the output type. With any, TypeScript would stop protecting the connection between input and output. An empty array explains the undefined possibility.
Type / syntax
Read it as
Used for
void
No useful returned value
addBook in the Week 2 service.
boolean
true or false
checkoutBook and returnBook report success.
unknown
A value we must inspect before use
Checking untrusted topping entries in HW1.
any
Disable type checking for this value
A broad input can still be runtime-checked; prefer precise types when possible.
field?: string
The field may be absent
Optional order customizations or update fields.
Partial<Product>
Every Product field becomes optional
Week 3 updates and Week 4 supplied fields.
Omit<Book, "id">
All Book fields except id
Describe editable book data.
Partial<Omit<Book, "id">>
Optional changes without the identity field
Week 2 updateBook signature.
Promise<Product | null>
An eventual product or absence
Week 4 asynchronous get service.
The JavaScript toolbox behind every week
Operation
What it returns / does
Example meaning
find(predicate)
First match or undefined
Find one menu item by id.
filter(predicate)
New array of all matches
Keep books by an author; remove a book by keeping the other ids.
map(callback)
New array with one transformed result per item
Documents → Products, or Products → manufacturer names.
forEach(callback)
Runs work for each item; returns undefined
Collect every validation problem.
some(predicate)
Boolean: at least one match
Does a size option exist?
every(predicate)
Boolean: all elements pass
Do all filtered items have the requested category?
includes(value)
Boolean membership
Is a crust offered?
Object.assign(book, changes)
Copies fields into an existing object
Week 2 partial updates mutate the found book.
{ ...fields, id }
Copies enumerable fields into a new object; later keys win
Replace a candidate id with the inserted id.
const { id, ...changes } = body
Separate id from remaining properties
Week 2 PATCH body.
value ?? fallback
Fallback only for null or undefined
Preserve price 0, unlike a truthiness-based default.
An arrow function such as book => book.author === filter is a callback supplied to the array method. === compares without type coercion: the number 1937 differs from the string "1937". async always returns a Promise; await pauses that async function until the Promise settles. It does not freeze the entire Node server.
With these projects’ ES-module/NodeNext setup, local TypeScript imports commonly end in .js, because Node loads the compiled JavaScript. import type brings in declarations erased at runtime; an ordinary import brings in usable runtime code. HW1 uses its supplied extensionless import configuration, so follow the repository’s existing setup.
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.
03
Week 2 · routers, middleware, services, and first tests
Order is part of the behavior.
Organize routes without changing the API
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.
A Router collects related handlers. The app supplies a mount prefix and 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.
Week 2 separates types.ts (Book shape), books-service.ts (private in-memory catalog and operations), books-router.ts (HTTP handlers), app.ts (assembly), and index.ts (listen on 6790). The service exports addBook, getAllBooks, getBook, updateBook, removeBook, and _resetCatalog. HTTP code asks the service for data rather than owning the array.
Ordinary middleware takes (req, res, next). Calling next() hands control to the next applicable handler. Sending a response finishes the request. Doing neither leaves it waiting. next(err) sends control forward to error middleware. Router-wide middleware applies to matching router traffic; route-specific middleware applies only to its route.
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 a deliberately simple classroom check, less strict 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.
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.
GET /books/:id
Public; convert path id to a number
200 with book; 404 when none exists.
PATCH /books
Auth; id in JSON body + at least one changed field
400 for unusable id/no changes, 404 when absent, 200 on update. Other fields remain.
DELETE /books
Auth; id in JSON body
200 after removal; this implementation also returns 200 when no book matched.
GET /books/badroute
Public teaching route
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 runtime object.
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);
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. A 404 handler is ordinary fall-through middleware for requests no route handled; an error handler handles an error being passed through the chain.
Tests import app.ts so importing the app does not start a fixed-port listener. Supertest manages the request’s temporary listener; you do not need to run npm run dev. This tests app behavior without executing index.ts, so a separate manual startup check still matters.
The Week 2 test checklist
The tests demonstrate an empty catalog, exact author filtering, rejected missing/wrong tokens, blank-title validation, 201 creation, GET by id/404, PATCH authentication and validation with unchanged fields, and DELETE authentication plus an empty final list. beforeEach calls _resetCatalog() so previous tests’ books do not affect the next one.
Use res.body for parsed JSON and res.text for plain text. Check both status and response content; seeing a plausible book alone does not prove that the endpoint returned the correct code.
What happens to POST /books with no token and a blank title?
403 in Week 2. The auth middleware answers before the title/author validator runs, assuming the submitted text was valid JSON.
TypeScript setup, headers, and all three NYT blocks
Install Express as a runtime dependency; TypeScript, tsx, @types/node, and @types/express supply development tools/declarations. Vitest, Supertest, and @types/supertest are test dependencies. tsx runs TypeScript during development, but it does not replace tsc --noEmit. Keep generated dist and installed node_modules out of Git with .gitignore.
The request start line contains method/path/protocol, and the response start line contains protocol/status. Both have headers, a blank line, then a body where appropriate. HTTP header names are case-insensitive; Node exposes incoming keys in lowercase. Add My-Header-Field: Woot! in Postman and inspect req.headers['my-header-field']. Compare headers from a browser and Postman; their clients provide different metadata.
The intermediate lecture POST/test initially use 200, then NYT explicitly asks for the appropriate creation status, 201. Your completed Week 2 code and tests use 201. The lecture/local error handler shows a stack trace for learning; HW1 instead sends a generic 500 and logs details on the server.
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.
NYT #2 · route behavior
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.
04
Homework 1 · menu, role checks, order validation, and errors
Put the pieces together: SliceDrop.
Four endpoints; two different roles
HW1 is an in-memory menu/orders API on port 3000. It combines Week 1 inputs with Week 2 routers, middleware, and tests. It has no MongoDB persistence. Menu data is supplied; stored orders get server-created id, status, and timestamp.
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.
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.
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];
}
Header value
Parser result
What happens next
Missing
null
401
Bearer slicedrop-customer-secret
Candidate token string
Customer route accepts it; staff route rejects it.
bearer token, Basic token
null
Case/scheme does not match the exact assignment format.
Bearer , Bearer one two
null
Empty token or extra whitespace is rejected.
Bearer nope
"nope"
Parsing succeeds; role middleware still rejects with 401.
^ and $ anchor the regular expression to the entire value. One literal space follows Bearer. [^\s]+ captures one or more non-whitespace characters. The parser is a plain function; the two middleware functions read the header, compare the candidate with their role’s constant, and either answer 401 or call next.
The app’s chain is JSON parser → menu/orders routers → JSON 404 fallback → four-argument error handler. For POST /orders, the router runs customer auth → handler → validateOrder → addOrder. With no token and valid JSON {}, the result is 401 before field validation.
Validate structure first; collect every safe-to-find error
Field / rule
Valid condition
Reason for the guard
Body
Non-null, non-array object
A null/array/primitive cannot be treated as an order object.
customerName
String whose trim() is nonempty
Reject absent, wrong-type, and whitespace-only names.
items
Nonempty array
Check Array.isArray before iterating.
Each item
Object with an existing menuItemId
Guard before accessing menu options.
quantity
Integer at least 1
Reject absent, text, zero, negative, and fractional quantities.
size / crust / sauce
If supplied, correct string and offered by that menu item
An optional field is still validated when present.
toppings
If supplied, array whose every entry is an offered string
A string is not a topping list; unknown/wrong-type entries are errors.
validateOrder returns string[]. It does not send an HTTP response. 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.
Use errors.push(...) to accumulate problems. If items is not an array, return after reporting the name/items issues rather than attempting an unsafe loop. If a menu lookup fails, report the id and quantity issues that can be checked, then skip option checks for that item. Returning inside a forEach callback stops that callback for the current item; it does not return from the outer validator.
The supplied helpers use some for offered sizes and includes for offered crusts, sauces, and toppings. A missing options list means the option is not offered. The validator must check runtime values even though NewOrder has TypeScript fields.
JSON-shaped illustration · comments are explanatory, not valid JSON
// A valid JSON body, but an invalid order:
{
"customerName": " ",
"items": [{ "menuItemId": "not-on-menu", "quantity": 0 }]
}
// With the customer token: 400 and multiple validation errors.
// Without it: 401 before order validation.
State, time, and three different failures
NewOrder describes customer-submitted fields. Order extends NewOrder adds id, status, and createdAt. addOrder creates ids such as "1" and "2", sets status to pending, uses an ISO timestamp, and pushes into the in-memory array. Unfiltered listing preserves insertion order; status filtering keeps exact matches. Restarting Node clears the orders and resets the counter.
Case
Where it fails
Response
Malformed JSON text
JSON parser → final handler
400; {"error":"Request body must be valid JSON"}
Valid JSON, invalid order fields
Plain validateOrder → route handler
400; {"errors":[...]}
Missing / wrong-role / bad token
Role middleware
401; {"error":"Unauthorized"}
Unknown method/path
Fall-through middleware
404; {"error":"Not found"}
Unexpected application exception
Final handler
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.
File map and homework test coverage
File / group
Study job
app.ts / index.ts
Parser, mount points, fallback, errors; separate listener.
middleware/auth.ts
Exact Bearer parsing and distinct customer/staff gates.
routers/menu.ts / orders.ts
Translate each HTTP request into lookups, validation, and data calls.
validation/validate-order.ts
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.
05
Week 3 · MongoDB, BSON, CRUD, ids, and Express integration
Move state into a database.
Choose a data model; learn the vocabulary
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.
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 mongodb npm package is a driver, not the server. MongoDB Atlas is a hosted service. Mongo can also support replication/failover and sharding; the class exercise uses one local server.
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();
Connecting to the server, selecting a database, and selecting a collection are distinct steps. A database may not appear in listings until it has stored data/a collection. Close clients when scratch work or tests finish; 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. If insertion remains enabled, every save/restart can add duplicates. Seed once, then turn the insertion call off before exploring reads/updates.
Create one/several documents. Results report acknowledged and insertedId or insertedCount/insertedIds; not a complete read-back document.
find(query)
Returns a Cursor, not the final array. Use await cursor.hasNext()/next() or await cursor.toArray().
findOne(query)
Returns one matching document or null; no-match is not an exception.
Equality query
{name: "Duct Tape"} matches equal field values.
Comparison query
$lt less than, $gte greater than or equal; $gt and $lte are the corresponding other boundaries.
updateOne / updateMany
Apply changes to first/all matches. {$set:{price:4.29}} preserves other fields and can add a field.
matchedCount / modifiedCount
A matching record can already have the requested value: matchedCount 1, modifiedCount 0. Use matches to decide existence.
deleteOne / deleteMany
Remove first/all matches; inspect deletedCount. deleteMany({}) removes every document in that collection.
Other driver options
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. One-operation methods affect at most one match; many-operation methods affect all matches. Verify an update by reading afterward, not just by assuming that a returned result contains the changed document.
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)
});
A standard Mongo-generated ObjectId is a 12-byte value commonly displayed as 24 hexadecimal characters. The URL carries a string. A query against an ObjectId-valued _id needs new ObjectId(id); an ordinary string has a different BSON type. 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.
Wrap Mongo operations with HTTP routes
db.ts owns connect/disconnect and product operations. It accepts URI/database arguments so the same code can use a practice database or a temporary test database. The router owns request fields and HTTP statuses. app.ts parses JSON and mounts /products. index.ts awaits connection before listening on 6790.
Week 3 route
Database function
Local success / absence
GET /products
getAllProducts → find().toArray()
200 with array, including [] when empty.
GET /products/:id
getProduct → findOne(_id)
200 with document; 404 if null.
POST /products
addProduct → insertOne()
201 with {id}.
PATCH /products/:id
updateProduct → $set, return matchedCount
200 if matched, even for a no-op; 404 if zero matches.
DELETE /products/:id
deleteProduct → deletedCount
200 if deleted; 404 if no match.
GET/PATCH/DELETE id routes first use validateId, so malformed ids receive 400. Week 3’s Product fields are name, price, and quantity. Its raw document responses expose _id; Week 4 adds the mapping to the public id field. ProductUpdate makes fields optional for PATCH. POST builds the known three-field product; PATCH uses supplied changes. Type annotations alone do not enforce runtime body validation.
_clearProducts() uses deleteMany({}) and is a test helper, not a route. Only call it after deliberately connecting to the isolated test database.
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 document fields in Compass.
NYT #2 · PATCH tests
Assert 404 for well-formed absent id; successful update changes only supplied fields and preserves the others.
NYT #2 · DELETE tests
Assert both success 200 and absent id 404; verify the document/list afterward.
NYT #2 stretch · id middleware
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.”
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.”
Constraint
What to understand
Uniform interface
A consistent way to identify resources and interact with their representations; appropriate HTTP methods/naming help.
Client–server
Separate client presentation from server responsibilities so each can evolve.
Stateless
Each request supplies the context required to understand it; do not depend on remembered conversational context from earlier requests.
Cacheable
Responses indicate whether they may be reused; cache decisions affect freshness.
Layered system
Interactions may pass through layers with limited visibility/responsibility. The lecture uses code layers to practice separation of concerns.
Code on demand · optional
A server can send executable code to extend the client, when appropriate.
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.
Design entities → representations → endpoints
Begin with the entities and their relationships. Then choose the fields clients see. Then lay out plural resource nouns, using HTTP methods for actions. The database’s storage arrangement should not dictate every public path.
Representation in the lecture 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.
Give each layer one responsibility
RoutesWhich method/path uses which controller?
ControllersRead HTTP inputs; choose status and response.
ServicesCoordinate product operations and model conversion.
ModelsDescribe public shapes; convert document/field data.
DatabaseConnect and execute Mongo operations.
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. Models are helpers used by the service, rather than another request endpoint.
Build bottom-up: database operations first, then model, service, controller, router, and app/startup. Database code needs no Express. Services know about database/models but not req/res. Controllers know about HTTP and services but do not query Mongo directly. Routes simply connect paths/middleware to controllers.
Week 4 source file
Responsibility
db/db.ts
init, getAllInCollection, getInCollection, addToCollection, deleteFromCollection; PRODUCTS constant; disconnect/_clearCollection for tests.
Map GET/list, POST, GET/:id, DELETE/:id; register validation middleware.
middleware/*
Id/body validation and final error handling.
app.ts / index.ts
Assemble parser/router/fallback/error chain; await database init before listen.
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) });
Id validation stops bad shapes before Mongo. The database returns a document or null. The service converts an existing document to Product or passes back null. Only the controller converts absence to 404, because 404 is an HTTP decision.
productFromDocument explicitly selects public fields and maps document._id.toString() to id. Clients get a string id and no _id. Mapping each document with .map() produces the list response. This converter is a plain function without a database call or req/res, so it can be tested in isolation.
Trace a request
Choose a scenario to see where it ends. This is a teaching simulation; it sends no network request.
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. The controller returns 201 with {id}. 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, but the insertion path uses the actual Mongo-generated id. 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. This is a local refinement of the lecture’s simpler insertion example. Local body middleware rejects arrays/non-objects and wrong supplied field types; price/quantity must be finite nonnegative numbers and quantity an integer. Omitted fields remain allowed.
For DELETE, the database returns deletedCount; the service reports a boolean; the controller answers 204 with no body when deleted, or 404 when absent. 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.
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.
Lecture · Supertest setup
Make init accept URI/db name; start temporary Mongo once, clear/seed before each test, disconnect/stop afterward.
Lecture · list/create assertions
Exactly two seeds, expected manufacturers/order, id shape, no _id; POST then list has three and includes the new manufacturer.
NYT #2 · 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 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.
Run describe/it tests, hooks, expect matchers, and mocks.
A passing suite covers only the assertions it contains.
Supertest
Build HTTP requests against the exported Express app; inspect status/body/text.
Importing app does not execute index.ts startup or test a real fixed-port server.
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.
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();
});
Hook
When
Why
beforeAll
Once before the suite/block
Expensive database start and connection.
beforeEach
Before every test
Known state: reset or clear + seed.
afterEach
After every test
Restore mocks or one-test changes.
afterAll
Once after the suite/block
Close clients and stop the process.
Week 2 resets the catalog array; HW1 resets orders; Week 3 starts with an empty products collection; Week 4 clears and inserts two known coffee makers. Those are distinct fixtures, not contradictory tests. 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. Start once, reset cheaply before each test, and always clean up what you started. Await asynchronous setup and requests so assertions run after the work completes.
Choose a matcher that asks the right question
Matcher
Question
Use / trap
toBe(200)
Is this the same value?
Good for primitive statuses/strings/booleans; object identity differs from equal contents.
toEqual({...})
Are the nested contents equal?
Objects/arrays; array order matters.
toHaveLength(2)
How many elements/characters?
Check array/string size.
toContain("Braun")
Is this value present?
Primitive membership/substrings; use toContainEqual for a newly constructed object.
.not.toContain("REVOTRA")
Is this value absent?
Pair with positive assertions; an empty wrong list also passes an absence-only test.
toMatch(/^[0-9a-f]{24}$/)
Does the string have the required shape?
Generated id: test its format rather than one exact unpredictable value.
toBeUndefined()
Is the property/value undefined?
Week 4 public products should not expose _id.
toBeNull()
Is this null?
No-match service/database result.
toBeDefined() / toBeTruthy()
Present / truthy?
These are broader than checking exact content or identity.
toThrow()
Does a function throw?
Pass a function; for async rejection use await expect(promise).rejects.toThrow().
expect({a:1}).toBe({a:1}) fails because they are separately created objects. toEqual succeeds. An HTTP request to a throwing route usually produces a 500 response you should assert; a direct unit call that rejects is where rejects.toThrow fits.
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);
});
});
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.
Read a failure; understand a pass
Start with the failing test’s name and expected/received values. An assertion failure means the run got far enough to compare behavior. A connection/download/import error is a setup problem and may prevent any request from running. A timeout can mean an unawaited operation, waiting for an absent Mongo server, or a request handler that neither answers nor continues.
Passing tests do not prove that TypeScript compiles, the real startup awaits Mongo, your imported sample data is present, or an instructor has graded the assignment. Run the separate typecheck/build commands and manually start the app when checking the full path. Classroom placeholder checks are not an API regression suite.
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.
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.
Lab A · HTTP and TypeScript
1. Label every input: POST /tracks?preview=true with JSON title and an Authorization header.
Method POST; path /tracks; req.query.preview is query input; req.body.title is parsed body input; req.headers.authorization is header input. There is no path parameter in this particular route.
2. Write /browse/:category and a query filter, then explain /browse/frozen?keywords=organic+corn.
JavaScript
app.get('/browse/:category', (req, res) => {
res.send(`Searching for ${req.query.keywords} in the ${req.params.category} category`);
});
category is frozen; keywords decodes to organic corn. This is one route pattern serving many categories.
3. Why must a ZIP code be a string? Why do numeric year filters need conversion?
A ZIP is an identifier that may begin with zero. A book year is a number in the model, while a query arrives as text; strict equality requires converting the single-string query to a number before comparison.
4. Recreate all six TypeScript exercise features from memory.
Book interface; catalog: Book[]; BookStatus enum; typed parameters/returns; BookFilter = string | number with typeof narrowing; generic getFirstItem<Item> returning Item | undefined. Compare your runtime demo with the unchanged JavaScript output.
5. What does getFirstItem([]) return? Can const catalog still receive a push?
undefined for an empty array. Yes: const prevents reassigning catalog, not mutating the array. A precise annotation or nonempty input supplies the element type for inference.
6. Convert "lcd,smart,sony" into the Week 1 display text.
JavaScript
const terms = 'lcd,smart,sony'.split(',').join(' AND ');
// lcd AND smart AND sony
Lab B · middleware and HW1
7. Predict Week 2 POST /books with wrong auth and blank title; then HW1 POST /orders with no auth and {}.
403 for Week 2, 401 for HW1. Both stop at route auth before field validation. If the JSON text itself is malformed, the parser stops earlier.
8. Does parseBearerToken("Bearer nope") return null?
No. It returns "nope" because the format is valid. Role middleware then rejects the candidate. Parsing and authorization answer different questions.
9. Build an invalid order with at least three errors. How should the server respond?
Use a blank customerName, unknown menuItemId, and quantity 0, with the customer token. Expect 400 and an errors array containing the safe-to-find problems. Without that token, expect 401. Do not invent option errors by reading an absent menu item.
10. Trace a request to a path nobody registered.
The parser runs where applicable, routers do not match, then the ordinary final fallback sends 404. No exception is necessary. The four-argument error handler is for propagated errors.
11. Create an order, restart Node, then list orders. Predict the result.
The array is process-local, so the newly started app has no saved orders. With staff auth, GET returns 200 and []. Mongo-backed storage in later exercises solves persistence across app restarts.
Lab C · all four direct Mongo tasks and API checks
12. Write findPricierTest and predict the original tape matches.
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.
09
Active recall · 42 questions with answer reveals
Close the notes. Explain it.
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
Term
Meaning
Endpoint / route
A client-accessible operation identified by method and path pattern.
Middleware
A function in the request chain that can continue, respond, or forward an error.
Callback
A function given to another function to run at the appropriate point.
Serialization
Converting a value into transferable text/binary form, such as JSON.
Contract
The agreed input/output behavior that clients and tests rely on.
Persistence
Stored state survives the application process restarting.
Cursor
An object through which database query results are retrieved/iterated.
Fixture / seed
Known data arranged so a test has an unambiguous expected result.
Mock
A test replacement for behavior, such as deliberately throwing an error.
Layer / separation of concerns
One code area owns one responsibility with clear boundaries.
Representation
The client-visible copy of a resource’s state.
Runtime / compile time
When the program executes / when its types and source are checked.
Module / import / export
A file’s reusable code and the declarations connecting it to other files.
Dependency / dev dependency
A package needed by the app / tooling used to develop, build, or test it.
Lockfile
Records resolved package versions to reproduce dependency installation.
Watch mode
Reruns/restarts work when watched files change.
Assertion
A test statement of expected behavior, checked by a matcher.
Test isolation
A test’s result does not depend on leftovers from another test/run.
11
Coverage, original guides, and source library
Every topic has a trail.
What was reviewed
Prepared September 29, 2026, from all seven local coursework folders, their available guides, source files/tests, and all four supplied course pages. The local Homework folder contains HW1; no local HW2/HW3 was present. This is a consolidated technical study guide for Weeks 1–4, 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.
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.