018 / SI 679 · Fall 2026

The backend
field guide.

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.

Weeks 1–4 + TypeScript + HW142 recall questions6 complete original guides
One continuous page. Learn at your pace.Find a concept /

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.

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

Know where the input lives

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

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

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

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

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.

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

02

TypeScript basics · the book catalog exercise

Types describe your assumptions.

What TypeScript changes

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

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.

Narrow a union before using it

TypeScript
type BookFilter = string | number;

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

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

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

The JavaScript toolbox behind every week

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

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.

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

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.

Middleware either continues or answers

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

booksRouter.use(express.json());
booksRouter.use(logger);
booksRouter.post('/', checkAuth, validateBookParams, handler);
Parse JSON→Log→Auth→Validate→Create

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

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

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

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

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

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

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

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

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

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

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

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

04

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

Put the pieces together: SliceDrop.

Four endpoints; two different roles

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

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

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

HTTP · classroom token from supplied constants
POST /orders
Authorization: Bearer slicedrop-customer-secret
Content-Type: application/json

{
  "customerName": "Ada",
  "items": [{ "menuItemId": "soda", "quantity": 1 }]
}

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 valueParser resultWhat happens next
Missingnull401
Bearer slicedrop-customer-secretCandidate token stringCustomer route accepts it; staff route rejects it.
bearer token, Basic tokennullCase/scheme does not match the exact assignment format.
Bearer , Bearer one twonullEmpty token or extra whitespace is rejected.
Bearer nope"nope"Parsing succeeds; role middleware still rejects with 401.

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

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.

CaseWhere it failsResponse
Malformed JSON textJSON parser → final handler400; {"error":"Request body must be valid JSON"}
Valid JSON, invalid order fieldsPlain validateOrder → route handler400; {"errors":[...]}
Missing / wrong-role / bad tokenRole middleware401; {"error":"Unauthorized"}
Unknown method/pathFall-through middleware404; {"error":"Not found"}
Unexpected application exceptionFinal handler500; {"error":"Internal server error"}, with details logged on the server.

The parser error is recognized as a SyntaxError with a body property. An unexpected exception is not relabeled as the client’s validation error. Test a timestamp’s type and whether Date.parse accepts it rather than asserting an exact clock time.

File map and homework test coverage

File / groupStudy job
app.ts / index.tsParser, mount points, fallback, errors; separate listener.
middleware/auth.tsExact Bearer parsing and distinct customer/staff gates.
routers/menu.ts / orders.tsTranslate each HTTP request into lookups, validation, and data calls.
validation/validate-order.tsAccumulate field errors; call offered-option helpers safely.
data/menu.ts / data/orders.tsSupplied menu lookup; process-local order state and test reset.
types.ts / constants.tsShared request/stored shapes; exercise tokens and port.
auth.test.tsMissing/bad/wrong-role tokens; authorized cases; auth before validation.
menu.test.tsSix items, category filters, customization fields, and missing id.
orders.test.ts201, unique ids, pending status, parseable time, list/filter/insertion order.
errors.test.ts404, all validation errors, malformed JSON, and a mocked addOrder failure → 500.

_resetOrders() runs before each order test. A mock that makes addOrder throw tests the error path without needing a real application fault. Restoring a mock prevents it from affecting later tests. Read the full original homework teaching guide in the source library for its file-by-file walkthrough and 18-question quiz.

Why is /menu?category=unicorn a 200 while /menu/unicorn is a 404?

The first asks for a collection filter and correctly gets no matches. The second asks for one particular resource that does not exist.

Source trail · Local HW1 implementation, all four test files, README, and the complete TEACHING_GUIDE.

05

Week 3 · MongoDB, BSON, CRUD, ids, and Express integration

Move state into a database.

Choose a data model; learn the vocabulary

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.

MongoDBRough relational comparisonExample
DatabaseDatabaseweek3db, week3app, week4
CollectionTableproducts
DocumentRowOne tape or coffee-maker record
FieldColumnname, price, quantity

Mongo stores BSON, a binary representation supporting additional types such as ObjectId and dates. JSON itself supports several value types; it does not make every value a string. Field names are strings, and nested objects/arrays let one document hold structured data. The Node driver translates between JavaScript values and BSON.

A local Community Edition server is mongod; mongosh is an interactive client, and Compass is a GUI client. The mongodb npm package is a driver, not the server. MongoDB Atlas is a hosted service. Mongo can also support replication/failover and sharding; the class exercise uses one local server.

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.

CRUD uses query objects and update operators

TypeScript
const products = db.collection('products');

const inserted = await products.insertOne({ name: 'Duct Tape', price: 5.99, quantity: 120 });
const all = await products.find().toArray();
const one = await products.findOne({ name: 'Duct Tape' });
const pricey = await products.find({ price: { $gte: 5 } }).toArray();

const changed = await products.updateOne(
  { _id: inserted.insertedId },
  { $set: { quantity: 150 } }
);
const removed = await products.deleteOne({ _id: inserted.insertedId });
OperationWhat to know
insertOne / insertManyCreate one/several documents. Results report acknowledged and insertedId or insertedCount/insertedIds; not a complete read-back document.
find(query)Returns a Cursor, not the final array. Use await cursor.hasNext()/next() or await cursor.toArray().
findOne(query)Returns one matching document or null; no-match is not an exception.
Equality query{name: "Duct Tape"} matches equal field values.
Comparison query$lt less than, $gte greater than or equal; $gt and $lte are the corresponding other boundaries.
updateOne / updateManyApply changes to first/all matches. {$set:{price:4.29}} preserves other fields and can add a field.
matchedCount / modifiedCountA matching record can already have the requested value: matchedCount 1, modifiedCount 0. Use matches to decide existence.
deleteOne / deleteManyRemove first/all matches; inspect deletedCount. deleteMany({}) removes every document in that collection.
Other driver optionsreplaceOne replaces a document; {upsert:true} can insert if absent; {$inc:{quantity:1}} increments a field. These are lecture extensions, not required route features.
TypeScript
const cursor = db.collection('products').find({ price: { $lt: 5 } });
const results: Document[] = [];
while (await cursor.hasNext()) {
  const doc = await cursor.next();
  if (doc) results.push(doc); // next() can be null
}

Names can be duplicated. A name filter illustrates queries, but targeting a known id is more precise for a one-resource API. 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 routeDatabase functionLocal success / absence
GET /productsgetAllProducts → find().toArray()200 with array, including [] when empty.
GET /products/:idgetProduct → findOne(_id)200 with document; 404 if null.
POST /productsaddProduct → insertOne()201 with {id}.
PATCH /products/:idupdateProduct → $set, return matchedCount200 if matched, even for a no-op; 404 if zero matches.
DELETE /products/:iddeleteProduct → deletedCount200 if deleted; 404 if no match.

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

ActivityWhat you must be able to write and verify
NYT #1 · findPricierTestFind all products with price ≥ 5 using $gte.
NYT #1 · updatePriceTestSet Scotch Tape’s price to 4.29 with updateOne and $set.
NYT #1 · deleteByNameTestDelete Masking Tape by name with deleteOne.
NYT #1 stretch · markOnSaleTestUse updateMany and $lt:5 to add onSale:true to the matching products; inspect differing document fields in Compass.
NYT #2 · PATCH testsAssert 404 for well-formed absent id; successful update changes only supplied fields and preserves the others.
NYT #2 · DELETE testsAssert both success 200 and absent id 404; verify the document/list afterward.
NYT #2 stretch · id middlewareReject malformed ids with 400 and test every affected GET/PATCH/DELETE route.

The current lecture notes say the MongoMemoryServer section was carried into Week 4. This guide groups the testing ideas together in Chapter 7 while preserving both weeks’ activities here and in the practice lab.

A PATCH sets price to its existing value. Should matchedCount 1 / modifiedCount 0 become 404?

No. A matching product exists; the operation is a successful no-op. 404 based only on modifiedCount would confuse “unchanged” with “absent.”

Source trail · Full supplied Week 3 notes; db-explore.ts, db.ts, product-router.ts, validate-id.ts, tests, and study plan.

06

Week 4 · REST, representations, and controller–service layers

Design the public contract.

REST is an architectural style

REST means Representational State Transfer, described by Roy Fielding in 2000. Its influence comes from fitting the web’s HTTP model with a consistent client/server interface. The lecture contrasts it with more elaborate distributed-call approaches such as SOAP/CORBA. REST is not simply “an endpoint that returns JSON.”

ConstraintWhat to understand
Uniform interfaceA consistent way to identify resources and interact with their representations; appropriate HTTP methods/naming help.
Client–serverSeparate client presentation from server responsibilities so each can evolve.
StatelessEach request supplies the context required to understand it; do not depend on remembered conversational context from earlier requests.
CacheableResponses indicate whether they may be reused; cache decisions affect freshness.
Layered systemInteractions may pass through layers with limited visibility/responsibility. The lecture uses code layers to practice separation of concerns.
Code on demand · optionalA 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 scenarioFields and relationship
Customerid, firstName, lastName, zipCode. zipCode is text, preserving leading zeros.
Productid, modelName, modelNumber, manufacturer, color, price, quantity.
Orderid, customerId, status, items keyed by product id with quantities.
RelationshipsA customer has orders; each order refers to a customer and contains quantities of products.
Endpoint designOperations
/customersGET collection; POST create.
/customers/:idGET one; PATCH supplied fields; DELETE one.
/productsGET collection; POST create.
/products/:idGET one; PATCH supplied fields; DELETE one.
/customers/:id/ordersGET this customer’s orders; POST a new order for this customer.

This is a design scenario. The Week 4 exercise implements product GET/list, POST, GET by id, and DELETE stretch; it does not implement the entire customer/order API or PATCH. Scenario policy: decrement stock when an order is submitted, check availability then, and store orders in a top-level collection so they can be queried across customers. Role permissions, searching, payments, and full validation are additional design work, not completed features of this exercise.

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 fileResponsibility
db/db.tsinit, getAllInCollection, getInCollection, addToCollection, deleteFromCollection; PRODUCTS constant; disconnect/_clearCollection for tests.
models/product.tsProduct, ProductFields, productFromDocument, productFromFields.
services/product-service.tsgetAll, get, add, remove; return app-shaped data or absence.
controllers/product-controllers.tsgetProducts, getProduct, addProduct, deleteProduct; decide 200/201/204/404.
routes/product-routes.tsMap GET/list, POST, GET/:id, DELETE/:id; register validation middleware.
middleware/*Id/body validation and final error handling.
app.ts / index.tsAssemble parser/router/fallback/error chain; await database init before listen.

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

    TaskRequired understanding / evidence
    Lecture · GET /productsImport products.json into week4/products; build all layers; receive two coffee makers with public id fields.
    Lecture · POST /productsSend raw JSON in Postman; receive 201/{id}; list grows to three; Compass confirms the stored document.
    NYT #1 · GET /products/:idAdd db/service/controller/route operations. Model converter already exists. Found id → 200 one object; valid absent id → 404; id string, no _id.
    NYT #1 stretch · DELETECarry delete through the layers; choose a success response; list gets shorter and Compass agrees.
    Lecture · Supertest setupMake init accept URI/db name; start temporary Mongo once, clear/seed before each test, disconnect/stop afterward.
    Lecture · list/create assertionsExactly two seeds, expected manufacturers/order, id shape, no _id; POST then list has three and includes the new manufacturer.
    NYT #2 · Write the testsdescribe GET /products/:id: 200 with the requested object; 404 for well-formed absent id. Get the existing id from the seeded list; compare objects with toEqual.
    NYT #2 stretch · DELETE testsDelete one; assert success then list length one. Absent id → 404. Action and verification are separate requests.
    Which layer should turn a missing document into HTTP 404?

    The controller. The database/service return absence, and the controller translates it into the HTTP contract.

    Source trail · Full supplied Week 4 notes (including “Write the tests”), local source/test refinements, and Week 4 study plan.

    07

    Weeks 2–4 + HW1 · Vitest, Supertest, database isolation, and matchers

    Make the evidence repeatable.

    Three tools, three jobs

    ToolJobWhat it does not prove
    VitestRun describe/it tests, hooks, expect matchers, and mocks.A passing suite covers only the assertions it contains.
    SupertestBuild HTTP requests against the exported Express app; inspect status/body/text.Importing app does not execute index.ts startup or test a real fixed-port server.
    MongoMemoryServerLaunch a real temporary mongod on a separate URI for database-backed tests.It is not the persistent local Mongo server required for Compass/manual use.

    A test-only URI/database keeps test data away from the normal week3app/week4 records. MongoMemoryServer is a real database process, not an in-memory JavaScript mock. Its temporary storage behavior depends on the Mongo/storage configuration; isolation comes from connecting your test code to its temporary instance. A random port is not an access-control boundary.

    The first run may download a Mongo binary; later runs use the cached binary. You need not start your normal mongod or npm run dev for these suites, but setup/download errors can still prevent a run. Tests against an ordinary persistent database can accumulate duplicate inserts between runs. A temporary instance solves cross-run leftovers; clear/reset hooks solve state left between tests within one run.

    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();
    });
    HookWhenWhy
    beforeAllOnce before the suite/blockExpensive database start and connection.
    beforeEachBefore every testKnown state: reset or clear + seed.
    afterEachAfter every testRestore mocks or one-test changes.
    afterAllOnce after the suite/blockClose clients and stop the process.

    Week 2 resets the catalog array; HW1 resets orders; Week 3 starts with an empty products collection; Week 4 clears and inserts two known coffee makers. 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

    MatcherQuestionUse / trap
    toBe(200)Is this the same value?Good for primitive statuses/strings/booleans; object identity differs from equal contents.
    toEqual({...})Are the nested contents equal?Objects/arrays; array order matters.
    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.

    Source trail · Week 2/HW1 tests, full Week 3/4 testing sections, and both Mongo test setups.

    08

    Practice lab · trace, predict, write, and check

    Try it before revealing it.

    Choose a study session

    Time availableA useful sequence
    30 minutes10 min HTTP inputs/statuses → 10 min layers/ObjectId → 10 min answer recall questions without notes.
    90 minutes15 min Chapters 1–2 → 15 min middleware/HW1 → 20 min Mongo CRUD → 20 min REST trace → 20 min tests/practice.
    Several sessionsOne chapter per session. Explain the code aloud, do its lab, then mark confidence only when you can reproduce the idea.

    For each prompt, predict first, then reveal the explanation. If you want to run code, use a practice copy or your isolated test database and restore temporary edits afterward. The page itself is a study tool and never calls your APIs or changes course data.

    Lab A · HTTP and TypeScript

    1. Label every input: POST /tracks?preview=true with JSON title and an Authorization header.

    Method POST; path /tracks; req.query.preview is query input; req.body.title is parsed body input; req.headers.authorization is header input. There is no path parameter in this particular route.

    2. Write /browse/:category and a query filter, then explain /browse/frozen?keywords=organic+corn.
    JavaScript
    app.get('/browse/:category', (req, res) => {
      res.send(`Searching for ${req.query.keywords} in the ${req.params.category} category`);
    });

    category is frozen; keywords decodes to organic corn. This is one route pattern serving many categories.

    3. Why must a ZIP code be a string? Why do numeric year filters need conversion?

    A ZIP is an identifier that may begin with zero. A book year is a number in the model, while a query arrives as text; strict equality requires converting the single-string query to a number before comparison.

    4. Recreate all six TypeScript exercise features from memory.

    Book interface; catalog: Book[]; BookStatus enum; typed parameters/returns; BookFilter = string | number with typeof narrowing; generic getFirstItem<Item> returning Item | undefined. Compare your runtime demo with the unchanged JavaScript output.

    5. What does getFirstItem([]) return? Can const catalog still receive a push?

    undefined for an empty array. Yes: const prevents reassigning catalog, not mutating the array. A precise annotation or nonempty input supplies the element type for inference.

    6. Convert "lcd,smart,sony" into the Week 1 display text.
    JavaScript
    const terms = 'lcd,smart,sony'.split(',').join(' AND ');
    // lcd AND smart AND sony

    Lab B · middleware and HW1

    7. Predict Week 2 POST /books with wrong auth and blank title; then HW1 POST /orders with no auth and {}.

    403 for Week 2, 401 for HW1. Both stop at route auth before field validation. If the JSON text itself is malformed, the parser stops earlier.

    8. Does parseBearerToken("Bearer nope") return null?

    No. It returns "nope" because the format is valid. Role middleware then rejects the candidate. Parsing and authorization answer different questions.

    9. Build an invalid order with at least three errors. How should the server respond?

    Use a blank customerName, unknown menuItemId, and quantity 0, with the customer token. Expect 400 and an errors array containing the safe-to-find problems. Without that token, expect 401. Do not invent option errors by reading an absent menu item.

    10. Trace a request to a path nobody registered.

    The parser runs where applicable, routers do not match, then the ordinary final fallback sends 404. No exception is necessary. The four-argument error handler is for propagated errors.

    11. Create an order, restart Node, then list orders. Predict the result.

    The array is process-local, so the newly started app has no saved orders. With staff auth, GET returns 200 and []. Mongo-backed storage in later exercises solves persistence across app restarts.

    Lab C · all four direct Mongo tasks and API checks

    12. Write findPricierTest and predict the original tape matches.
    TypeScript
    return await db.collection('products').find({ price: { $gte: 5 } }).toArray();

    With original prices 5.99, 3.99, and 2.01, only Duct Tape matches. $gte includes the exact boundary value 5.

    13. Write the Scotch Tape price update; what should not change?
    TypeScript
    return await db.collection('products').updateOne(
      { name: 'Scotch Tape' },
      { $set: { price: 4.29 } }
    );

    The name, quantity, and id stay unchanged. Check the matched/modified result and read the document afterward.

    14. Write deleteByNameTest and markOnSaleTest.
    TypeScript
    await db.collection('products').deleteOne({ name: 'Masking Tape' });
    
    await db.collection('products').updateMany(
      { price: { $lt: 5 } },
      { $set: { onSale: true } }
    );

    On the original fixture, both Scotch and Masking Tape are below 5. If you already deleted Masking Tape, only Scotch remains to match. Your prediction must reflect the current data state.

    15. Test PATCH: what proves it is partial?

    Create a tape, PATCH only price, GET it again, then assert the new price and unchanged name/quantity/id. Also assert 404 for a well-formed absent id and 400 for a malformed id. matchedCount, not modifiedCount, decides whether the product existed.

    16. Write a DELETE test that checks more than the status.

    Create or seed a product, delete it, check the week-specific success status, then GET it (404) and list products to prove it is absent. Delete an absent id for 404 and malformed id for 400.

    Lab D · Week 4 route and testing activities

    17. Add GET /products/:id to every required layer. Which layer does not need a new converter?

    Database adds lookup by ObjectId; service returns Product | null; controller reads params and chooses 200/404; route maps GET /:id. The existing productFromDocument model converter already handles the document.

    18. Write the two tests required by the “Write the tests” anchor.

    With the two seeded products, GET /products to obtain an existing id, then GET that id: 200 and toEqual the expected product object. For an absent well-formed id, GET it and assert 404. Both get the beforeEach fixture automatically. See the complete worked example in Chapter 7.

    19. Design customer/order routes without exposing the collection arrangement.

    GET/POST /customers/:id/orders expresses orders for a customer even when orders are stored in their own collection. Public paths model client needs; they do not have to mirror database nesting.

    20. Break one expected status in a test, run it, and read the failure. What should you restore?

    Use a practice copy or temporary edit, change the expectation to an intentionally wrong code, run the targeted test, and read expected/received output. Restore the correct assertion and confirm green. The aim is understanding failures, not weakening assertions.

    Source trail · Practice adapted from the linked NYT tasks and the six existing guides.

    09

    Active recall · 42 questions with answer reveals

    Close the notes. Explain it.

    Answer aloud before opening each card

    These questions cover the complete core path. A confident explanation should include an example, not just a remembered word. Use “Show all answers” above when reviewing or printing.

    1. What makes an API route unique?

    The HTTP method plus its path pattern. GET /orders and POST /orders are different routes.

    2. Which input source uses req.params, req.query, and req.body?

    Path placeholders, URL query pairs, and parsed submitted content respectively.

    3. How do HTML form inputs become object keys?

    Their name attributes choose submitted keys; action and method choose the endpoint.

    4. Why put body parsers first?

    Later middleware/handlers need parsed req.body; malformed data may fail in the parser first.

    5. What does return after res.status(...).send(...) do?

    Stop the current handler from continuing and attempting a second response.

    6. What does a union type mean?

    A value may be one of the named alternatives; narrow it with a runtime check before using type-specific operations.

    7. Interface versus runtime validation?

    An interface is erased compile-time information. Validation actually inspects incoming runtime values.

    8. Why return Book | undefined from a lookup?

    The array may contain no matching book.

    9. What does a generic preserve?

    The relationship between the input’s element type and the output type across different calls.

    10. What do Partial and Omit do?

    Partial makes fields optional; Omit excludes named fields from a compile-time type.

    11. What does await do?

    Wait within the async function for the Promise’s settlement. Rejection can propagate as an error.

    12. Which nullish values activate ??, and does zero activate it?

    Null and undefined activate it. Zero, false, and an empty string do not.

    13. Router mount /menu plus internal /:id equals what?

    Public /menu/:id.

    14. Two ordinary middleware outcomes?

    Call next() or end the response. next(error) forwards an error.

    15. What identifies error middleware?

    The four-parameter shape (err, req, res, next), registered after relevant routes.

    16. Why does middleware order change a status?

    An earlier middleware can finish the request before a later validator or handler runs.

    17. How do Week 2 and HW1 auth failures differ?

    The classroom Week 2 implementation uses 403; HW1 requires 401, including wrong-role tokens.

    18. How does a valid bearer format differ from a valid credential?

    Format lets the parser extract a candidate. Role middleware still has to accept that candidate.

    19. Why collect all order errors?

    The client can fix multiple fields together, while structural guards prevent unsafe checks.

    20. What should an unknown collection filter return?

    Usually the exercise’s 200 with []; it is different from an unknown single resource, which is 404.

    21. What is lost when an in-memory server restarts?

    Process-local state such as the catalog/order array and counter.

    22. Collection/document/field corresponds roughly to what?

    Table/row/column, without implying identical relational behavior.

    23. Does JSON contain only strings?

    No: it has strings, numbers, booleans, null, arrays, and objects. BSON adds richer database types.

    24. What does find return, versus findOne?

    find returns a cursor. findOne returns a document or null.

    25. What do $gte, $lt, and $set mean?

    At least, strictly less than, and change/add only the supplied fields.

    26. Why can modifiedCount be zero on a valid update?

    The document already contains the requested value; matchedCount still shows it existed.

    27. Why convert a URL string to ObjectId?

    The query must use the BSON type of the stored generated _id.

    28. Malformed id versus absent well-formed id?

    400 for unusable format; 404 when the valid query finds no resource.

    29. Why have connect/init parameters?

    The app and tests can use different URIs/databases without changing database-layer code.

    30. What makes a request stateless?

    It provides the context needed to interpret it, rather than depending on a prior request’s conversational state.

    31. What is a resource representation?

    A client-visible copy of resource state; changing the copy does not directly edit the server’s stored resource.

    32. Name REST’s six constraints.

    Uniform interface, client–server, statelessness, cacheability, layered system, optional code on demand.

    33. Where should HTTP statuses be chosen in Week 4?

    Controllers. Services/database return application data or absence.

    34. Where does _id become id?

    productFromDocument in the model layer converts ObjectId to a public string.

    35. What is special about a 204 response?

    Successful completion with no response body.

    36. Why does temporary Mongo not eliminate beforeEach?

    It is fresh between runs, but tests within one run can still share and mutate its data.

    37. Why seed GET tests directly through the database?

    So their setup does not depend on a working POST route.

    38. toBe versus toEqual?

    Same primitive/object identity versus equal nested contents. Use toEqual for separately created products.

    39. Why pair negative and positive assertions?

    An empty wrong result can satisfy “not containing X”; positive assertions prove the expected result exists.

    40. What is arrange–act–assert?

    Prepare known data, execute the operation, then check the observable result and state.

    41. Why assert generated values by shape/properties?

    Ids and timestamps vary. Test the required format/parseability rather than one exact run’s value.

    42. What startup behavior does Supertest miss?

    index.ts listener/database initialization and the normal deployment/runtime configuration.

    Source trail · Consolidated self-checks from every topic.

    10

    Commands, ports, status codes, troubleshooting, and vocabulary

    The desk reference.

    Run the right script in the right folder

    Open a terminal in the specific project folder. A script only exists when that repository’s package.json defines it. These commands are a reference; the study page does not execute them.

    ProjectScripts to knowNormal app / database
    Classes/week1hellonpm startPort 1679; no database.
    Classes/si-679-f-26-week1-nyt-kelvintigernpm startPort 3000; no database. Its npm test is a placeholder, not a local test suite.
    Classes/si-679-f-26-ts-basics-kelvintigertypecheck, build, start, dev, jsConsole catalog; no HTTP server; no npm test script.
    Classes/si-679-f-26-week02-nyt-kelvintigertest, test:watch, typecheck, build, dev, startPort 6790; in-memory catalog.
    Homework/si-679-f-26-hw1-kelvintigertest, test:watch, typecheck, build, devPort 3000; in-memory orders. No npm start script.
    Classes/si-679-f-26-week03-nyt-kelvintigertest, test:watch, typecheck, build, explore, dev, startPort 6790; app week3app; exploration week3db.
    Classes/si-679-f-26-week-04-nyt-kelvintigertest, test:watch, typecheck, build, dev, startPort 6790; week4/products; sampleData/products.json.
    CommandPurpose
    npm ciInstall the lockfile’s dependency set for an existing checkout with a valid lockfile.
    npm installInstall/update dependencies; used by the lecture starters.
    npm testUsually vitest run, once, in the repositories defining it.
    npm run test:watchRerun tests as source changes.
    npm run typechecktsc --noEmit checks types without generating files.
    npm run buildCompile TypeScript into dist. HW1 uses a build config excluding tests.
    npm run devRun TypeScript with tsx (usually watch); does not replace a typecheck.
    npm startRuns whatever start means in that project: watched JS or built dist.
    Shell
    # From the existing Week 4 repository:
    npm ci
    npm test
    npm run typecheck
    npm run build
    
    # For manual practice, separately start/connect your normal MongoDB,
    # then import sampleData/products.json into week4/products in Compass.
    npm run dev
    # Keep this terminal open; use a second one for curl. Stop with Ctrl-C.

    Default Mongo URI is mongodb://127.0.0.1:27017. A manually started server can use mongod --dbpath ~/data/mdata if that is the data directory you created. These are course setup instructions, not a claim that mongod/mongosh/Compass are currently installed or running on this Mac.

    A complete Week 4 manual round trip

    Shell · delete only the practice product you created
    curl -i http://localhost:6790/products
    
    curl -i -X POST http://localhost:6790/products   -H 'Content-Type: application/json'   -d '{"modelName":"Practice coffee maker","price":25,"quantity":3}'
    
    # Replace YOUR_ID with the id just returned above.
    curl -i http://localhost:6790/products/YOUR_ID
    curl -i -X DELETE http://localhost:6790/products/YOUR_ID
    curl -i http://localhost:6790/products/YOUR_ID

    Expect list 200, create 201, read 200, delete 204 with empty body, then read 404. -i shows response headers/status; -X chooses the method; -H adds headers; -d supplies the body. Use your own new practice record for deletion.

    Shell · supplied classroom credentials
    # HW1 manual calls (server on port 3000):
    curl 'http://localhost:3000/menu?category=pizza'
    curl -X POST http://localhost:3000/orders   -H 'Authorization: Bearer slicedrop-customer-secret'   -H 'Content-Type: application/json'   -d '{"customerName":"Ada","items":[{"menuItemId":"soda","quantity":1}]}'
    curl http://localhost:3000/orders   -H 'Authorization: Bearer slicedrop-staff-secret'

    Keep HW1’s POST and list in the same running process to see the order. Quote URLs containing & in your shell, and replace illustrative ids instead of copying them blindly.

    Statuses and contracts you should not blur together

    CodeMeaningCourse example
    200Successful responseGET/list; Week 1 tracks; Week 3 PATCH/DELETE.
    201Created a resourceWeek 2 books, HW1 orders, Weeks 3–4 products POST.
    204Success with no bodyLocal Week 4 DELETE.
    400Unusable client inputBad JSON/fields or malformed id.
    401Authentication credential not acceptedHW1 missing/bad/wrong-role token. General semantics differ from the exercise’s simplified role handling.
    403ForbiddenGeneral lack of permission; Week 2 uses it for all failed classroom token checks.
    404No requested route/resourceWell-formed absent id; unknown route. Empty filtered collections remain 200.
    500Unexpected server errorThrown application error; inspect server logs.
    DistinctionEarlier exampleLater / other example
    IdWeek 1 menu-style text / Week 2 numeric Date.nowMongo ObjectId represented as hex text in URLs.
    PATCH pathWeek 2 /books, body contains idWeek 3 /products/:id. Week 4 local exercise has no PATCH route.
    DELETE pathWeek 2 /books, body idWeeks 3–4 /products/:id.
    DELETE statusWeek 2/3 200Week 4 local 204; notes allow 200 or 204.
    StorageWeek 2/HW1 process-local arraysWeeks 3–4 persistent normal Mongo; temporary Mongo for tests.
    Response identityWeek 3 documents show _idWeek 4 public Product exposes id and omits _id.
    Product shapeWeek 3 name/price/quantityWeek 4 modelName/modelNumber/manufacturer/color/price/quantity/id.

    Debug from the outside inward

    SymptomLikely place to inspectNext check
    Connection refused immediatelyHTTP listener or wrong portIs the correct dev server running? Check 1679/3000/6790 and terminal errors.
    Long wait, then Mongo connection errorDatabase server / URIConfirm mongod reachable at the chosen URI, not just npm driver installed.
    Cannot POST /products / 404 HTMLMethod, mount, registered routeCheck POST vs GET and router relative path/prefix.
    Undefined body / empty defaultsContent-Type, body format, parserPostman raw JSON; express.json before route; inspect local validation response.
    GET list is []Chosen database/collection/fixtureweek3db vs week3app vs week4; did you import/seed into the same place?
    401 / 403 before data errorsAuth middlewareCorrect exact classroom credential/role; week-specific status.
    400 for YOUR_IDId shapeReplace the placeholder with a real returned 24-hex id.
    Headers already sentControl flowLook for continuing after a response or calling next after responding.
    Request hangsMiddleware/await/connectionDid every path call next or answer? Did the database operation finish?
    Test passes once but fails laterShared stateReset between tests; avoid persistent database leftovers; restore mocks.
    Type errors despite running dev serverCompiler vs runnertsx execution is separate from npm run typecheck.
    Port already in useAnother running appStop the conflicting process or use the project’s configurable port.

    Work in this order: method/full path → status → headers/body → parser/middleware → handler/service → database state → response. For a 500, read the log. For tests, read the failure’s expected/received values and fixture. Once fixed, rerun the relevant test and typecheck.

    Plain-language glossary

    TermMeaning
    Endpoint / routeA client-accessible operation identified by method and path pattern.
    MiddlewareA function in the request chain that can continue, respond, or forward an error.
    CallbackA function given to another function to run at the appropriate point.
    SerializationConverting a value into transferable text/binary form, such as JSON.
    ContractThe agreed input/output behavior that clients and tests rely on.
    PersistenceStored state survives the application process restarting.
    CursorAn object through which database query results are retrieved/iterated.
    Fixture / seedKnown data arranged so a test has an unambiguous expected result.
    MockA test replacement for behavior, such as deliberately throwing an error.
    Layer / separation of concernsOne code area owns one responsibility with clear boundaries.
    RepresentationThe client-visible copy of a resource’s state.
    Runtime / compile timeWhen the program executes / when its types and source are checked.
    Module / import / exportA file’s reusable code and the declarations connecting it to other files.
    Dependency / dev dependencyA package needed by the app / tooling used to develop, build, or test it.
    LockfileRecords resolved package versions to reproduce dependency installation.
    Watch modeReruns/restarts work when watched files change.
    AssertionA test statement of expected behavior, checked by a matcher.
    Test isolationA test’s result does not depend on leftovers from another test/run.

    Source trail · Scripts and source in the current seven local projects, plus course notes.

    11

    Coverage, original guides, and source library

    Every topic has a trail.

    What was reviewed

    Prepared September 29, 2026, from all seven local coursework folders, their available guides, source files/tests, and all four supplied course pages. The local Homework folder contains HW1; no local HW2/HW3 was present. This is a consolidated technical study guide for Weeks 1–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.

    Course-source coverage checklist

    Source sectionCovered materialStudy here
    Week 1 · Getting started / first route / routesNode/npm/ESM/watch, Express app/listen, method+path, response APIsOpen topic →
    Week 1 · POST / forms / form middlewareaction/method/name, req.body, URL-encoded parser, ZIP textOpen topic →
    Week 1 · Query strings / route params / errorsreq.query/params, decoding, numeric guards, 400/default 200Open topic →
    Week 1 · NYT #1catalog, play, getform/submitform, browseOpen topic →
    Week 1 · Postman / JSON / NYT #2postData, hithere, tracks/error/timestamp, products AND keywordsOpen topic →
    TypeScript basics · all six tasksInterface, annotation, enum, signatures, union/narrowing, genericOpen topic →
    Week 2 · Setting up TS / routing / NYT #1NodeNext/build/dev/typecheck, Router/service, combined exact filtersOpen topic →
    Week 2 · HTTP headers / middlewareHeader metadata/case/custom values, logger, auth, route orderOpen topic →
    Week 2 · Errors / statuses / NYT #2Four-argument handler, badroute, 201, GET by id, PATCH body idOpen topic →
    Week 2 · Supertest / app split / state leaks / NYT #3Request chains, reset, body/text assertions, auth/filter/delete casesOpen topic →
    HW1 · complete local implementation and guideMenu/filter, two roles, exact parser, all order validation, state/errors/testsOpen topic →
    Week 3 · What is Mongo / NoSQL / BSONSQL tradeoffs, collection/document/field, nesting/types, server vs driverOpen topic →
    Week 3 · Database / starter / connectionCompass/mongosh, URI, client/db/collection/ping/disconnect, watch duplicatesOpen topic →
    Week 3 · Inserts / finds / queries / updates / deletesinsertOne/Many, cursor/findOne, ObjectId, comparisons, $set/results/optionsOpen topic →
    Week 3 · NYT #1 + stretchPricier products, Scotch price, Masking deletion, onSale many updateOpen topic →
    Week 3 · Express / DB / router / app / server / PostmanAll CRUD routes, connection before listen, type shapes and statusesOpen topic →
    Week 3 · Live DB tests / temporary Mongo / matchersCross-run and cross-test contamination, hooks, matchers/rejectionsOpen topic →
    Week 3 · NYT #2 + stretchPATCH preserved fields/404, DELETE 200/404, malformed-id middleware testsOpen topic →
    Week 4 · REST / principles / scenarioSix constraints, representation, entities/fields/endpoints/policy boundariesOpen topic →
    Week 4 · Architecture / build GET / all layersLayer responsibilities, imports, converters, map, glue/errors/startupOpen topic →
    Week 4 · Build POST / DB/model/service/controller/router / PostmanPartial, ??, insertedId, 201/id, readback/Compass, troubleshootingOpen topic →
    Week 4 · NYT #1 + DELETE stretchGET single/404/public id, delete count/readback, controller HTTP decisionOpen topic →
    Week 4 · Supertest / temporary DB / init / hooks / seed / assertionsApp vs startup boundary, direct DB fixtures, matcher examplesOpen topic →
    Week 4 · NYT #2 / Write the tests / DELETE stretch testsExact requested-object/404 tests; successful/absent deletion and list stateOpen topic →

    The four pages you asked to cover

    Week 1 · Course Intro, Express Intro ↗

    Rendered-page topic/exercise extract

    Week 2 · Express Routing & Middleware, Unit Testing ↗

    Rendered-page topic/exercise extract

    Week 3 · MongoDB ↗

    Full main-page text snapshot

    Week 4 · REST APIs + Write the tests ↗

    Full main-page text snapshot

    All six existing guides, readable here

    Open these for the complete original walkthroughs, run instructions, practice prompts, and answer keys. They have been embedded and formatted, so you do not need a Markdown viewer. The original coursework files remain unchanged; readable source snapshots travel with this guide’s folder.

    TypeScript basics · complete original guide

    Original Markdown snapshot · Classes/si-679-f-26-ts-basics-kelvintiger/STUDY_PLAN.md

    TypeScript Basics Study Plan

    This exercise takes a working JavaScript book catalog and adds TypeScript one idea at a time. Read src/index.ts beside this guide.

    Study order
    • 1. Interface: Book describes the required fields and their types. Its

    status must be a BookStatus, so an unrelated string cannot be stored.

    • 2. Typed array: Book[] means the catalog may contain only books.
    • 3. Enum: BookStatus gives names to the three allowed status strings and

    prevents spelling variations throughout the program.

    • 4. Function signatures: parameter and return types document each function

    and let TypeScript check every call.

    • 5. Union type: BookFilter = string | number allows an author or a year.

    The typeof check narrows the union before comparison.

    • 6. Generic: getFirstItem<Item> preserves the item type for any array. A

    book array produces Book | undefined; a string array would produce string | undefined.

    findBookByTitle() and getFirstItem() may return undefined when no item exists. The demo uses optional chaining (?.) so that possibility is handled instead of ignored.

    Exercises and checkpoints
    • Run npm run typecheck. Explain why it prints no output when successful.
    • Run npm run build && npm start and compare every output line with the

    README.

    • Add a practice book with an invalid status string and read the compiler

    error. Remove it afterward.

    • Call findBooksBy() with a boolean and read the compiler error. Remove the

    call afterward.

    • Call getFirstItem() with [1, 2, 3] and hover over the result in your

    editor. TypeScript should infer number | undefined without any.

    The central idea is that TypeScript checks assumptions before the program runs. It does not replace the JavaScript program; the build step produces JavaScript that Node executes.

    Week 1 · complete original guide

    Original Markdown snapshot · Classes/si-679-f-26-week1-nyt-kelvintiger/STUDY_PLAN.md

    Week 1 Study Plan: Express and HTTP from the Beginning

    This guide explains the Week 1 class and the finished assignment in server.js. Work through it in order; each section depends on the one before it.

    What you should be able to do

    By the end of the week, you should be able to:

    • 1. explain the roles of a client, server, request, and response;
    • 2. define GET and POST routes in Express;
    • 3. find data in a query string, route parameters, form body, or JSON body;
    • 4. return a response body and an appropriate HTTP status;
    • 5. validate user input before using it;
    • 6. use Postman or curl to test routes that a browser address bar cannot test.
    1. Build the client/server mental model

    A client asks for something. A server receives the request, runs code, and sends a response.

    text
    client -> HTTP request -> Express route -> HTTP response -> client

    An HTTP request contains a method, a path, headers, and sometimes a body. For example:

    text
    GET /catalog?itemid=222

    GET is the method. /catalog is the path. itemid=222 is a query string. The server matches the method and path to this code:

    js
    app.get('/catalog', (req, res) => {
      // read the request and send a response
    });

    Checkpoint: explain why typing /submitform into a browser address bar cannot test a POST route. The address bar sends GET, while the route expects POST.

    2. Understand the Express setup

    At the top of server.js, express() creates the application. Two middleware functions prepare request bodies before routes use them:

    js
    app.use(express.urlencoded({ extended: true }));
    app.use(express.json());

    express.urlencoded() parses ordinary HTML form submissions. express.json() parses JSON sent by Postman or another program. After parsing, the submitted values are available in req.body.

    At the bottom, app.listen(3000) starts the server. Port 3000 is one numbered door on the computer. The full local address is http://localhost:3000.

    Exercise:

    • 1. Run npm start.
    • 2. Visit / and /about.
    • 3. Change one response string, save, and observe Node's watch mode restart the

    server.

    • 4. Restore the response before continuing.
    3. Learn the four places this assignment receives input
    Query strings

    GET /catalog?itemid=222 places 222 in req.query.itemid. Query strings are good for searches and filters because they do not identify a different route.

    The code verifies that itemid is one string, is not blank, and converts to a number. A bad value returns status 400 and the exact required message.

    Route parameters

    The colons in this route mark changing path segments:

    js
    app.get('/play/artist/:artist/song/:song', ...)

    For /play/artist/Radiohead/song/Creep, Express creates:

    js
    req.params.artist === 'Radiohead'
    req.params.song === 'Creep'

    URL encoding turns %20 into a space, so The%20Clash reaches the handler as The Clash.

    HTML form bodies

    GET /getform returns HTML containing a form. The form's action and method tell the browser to send a POST to /submitform. Each input's name becomes a key in req.body:

    js
    const { address, city, zipcode } = req.body;

    The ZIP code stays a string. That preserves a leading zero such as 01234.

    JSON bodies

    POST /tracks receives JSON. Postman must send Content-Type: application/json, and the server needs express.json() before the route. The route destructures the four fields from req.body and returns a JSON object with the added track and a timestamp.

    Checkpoint: for each assignment route, identify whether its inputs come from req.query, req.params, or req.body.

    4. Understand validation and status codes

    The catalog route returns 400 Bad Request when itemid is not numeric. The track route also returns 400 when artist or title is missing, is not a string, or contains only spaces.

    The pattern is:

    js
    if (inputIsInvalid) {
      res.status(400).send('helpful message');
      return;
    }

    The return matters. Once the server sends an error response, the handler must stop. Otherwise it may try to send a second response.

    Useful first-week statuses:

    • 200: the request succeeded;
    • 400: the client supplied unusable input;
    • 404: no route matches the requested method and path;
    • 500: application code failed unexpectedly.

    Exercise: test /catalog with 222, 3, xyz, a blank value, and no itemid key. Predict the status and body before sending each request.

    5. Walk through every assignment route
    RouteInput sourceMain transformation
    GET /catalogquery itemidvalidate number and place it in a sentence
    GET /play/artist/:artist/song/:songtwo route parametersplace both decoded values in a sentence
    GET /getformnonereturn an address form
    POST /submitformform bodycombine address, city, and ZIP code
    GET /browse/:categoryroute parameter + querycombine category and keywords
    GET /hitherequery namecreate a greeting
    POST /tracksJSON bodyvalidate and return a structured JSON result
    GET /products/:department/:categorytwo route parameters + queryturn comma-separated keywords into AND terms

    The product search calls split(',') to make an array and join(' AND ') to make the required display string. For example:

    text
    lcd,smart,sony -> ['lcd', 'smart', 'sony'] -> lcd AND smart AND sony

    The track response uses new Date().toISOString(). The exact time changes on every request, but the ISO format stays predictable.

    6. Suggested study session
    First 20 minutes: HTTP basics
    • Draw one request and response.
    • Label method, path, query string, headers, body, status, and response body.
    • Explain GET versus POST aloud.
    Next 25 minutes: trace the code
    • Read server.js from top to bottom.
    • For every route, write down where its inputs live.
    • Cover the handler and predict its response from only the request URL/body.
    Next 25 minutes: send requests
    • Create a Week 1 Postman folder.
    • Save one request for each route.
    • Test successful and invalid catalog/track cases.
    • Confirm the status as well as the visible body.
    Final 20 minutes: change and restore
    • Add one temporary route parameter to a practice route.
    • Add one temporary query-string filter.
    • Add one validation check that returns 400.
    • Test each change, then restore the submitted assignment code.
    Final self-check

    You are ready for Week 2 when you can answer these without opening the notes:

    • 1. What is the difference between a method and a path?
    • 2. Where do query parameters, route parameters, and submitted bodies appear

    on req?

    • 3. Why must body-parsing middleware come before the routes?
    • 4. Why does an error branch send a response and then return?
    • 5. What makes an input problem a 400 instead of a 500?
    • 6. Why is Postman useful for POST requests?

    Run the assignment with:

    bash
    npm install
    npm start
    Week 2 · complete original guide

    Original Markdown snapshot · Classes/si-679-f-26-week02-nyt-kelvintiger/STUDY_PLAN.md

    Week 2 Study Plan: Routers, Middleware, and Supertest

    This guide explains the Week 2 class and the finished books assignment. Read the files in this order: types.ts, books-service.ts, books-router.ts, app.ts, index.ts, and finally __tests__/books.test.ts.

    What you should be able to do

    By the end of the week, you should be able to:

    • 1. use TypeScript types in an Express project;
    • 2. split routes into an Express router;
    • 3. separate data operations from HTTP handlers;
    • 4. write middleware and explain why its registration order matters;
    • 5. use headers for simple token checks;
    • 6. choose appropriate HTTP status codes;
    • 7. split the Express app from the listening server;
    • 8. test requests and responses with Vitest and Supertest.
    1. Understand the project layers

    The application separates five responsibilities:

    text
    index.ts -> app.ts -> books-router.ts -> books-service.ts -> catalog array
                             |
                             -> types.ts describes the data
    • types.ts defines what a book looks like.
    • books-service.ts owns the in-memory catalog and its operations.
    • books-router.ts translates HTTP requests into service calls.
    • app.ts assembles the Express application and final error handler.
    • index.ts opens port 6790 when the app runs as a real server.

    This separation makes the code easier to understand and lets tests import the app without starting a fixed network listener.

    Checkpoint: explain why a route should call getBook(id) instead of reading the catalog array directly.

    2. Review the TypeScript pieces

    The Book interface requires an ID, title, author, year, and status. BookStatus limits status to the supplied enum values. The service function signatures show what every operation accepts and returns.

    Project-local imports end in .js, even though the source files end in .ts:

    ts
    import { getAllBooks } from './books-service.js';

    TypeScript compiles the files to JavaScript, and Node ultimately loads the .js files. The NodeNext configuration checks imports using those Node ES module rules.

    updateBook() accepts Partial<Omit<Book, 'id'>>:

    • Omit<Book, 'id'> means all book fields except the ID;
    • Partial<...> makes those remaining fields optional.

    That type fits PATCH: a request may change only one field, but it should not replace the book's identity.

    3. Learn the service layer

    books-service.ts keeps the catalog private and exports focused operations:

    FunctionJob
    addBookadd one book
    getAllBooksreturn the catalog
    getBookfind a book by numeric ID
    updateBookapply selected changes to one book
    removeBookremove the matching ID
    _resetCatalogempty test state before each test

    The catalog uses let because removing or resetting books replaces the array. removeBook() uses filter() to keep every book whose ID does not match.

    The leading underscore in _resetCatalog() signals that application routes should not call it. It exists so tests remain independent.

    Exercise: start with three book IDs on paper and walk through removeBook() for the middle ID. Write the array that filter() returns.

    4. Follow middleware in order

    Middleware is a function that receives req, res, and next. It either:

    • calls next() so Express continues; or
    • sends a response and stops the chain.

    The router-wide order is:

    text
    express.json() -> logger -> matching route

    The POST route adds two route-specific steps:

    text
    JSON parser -> logger -> checkAuth -> validateBookParams -> POST handler

    Auth runs before validation. A request with no valid token and a bad book body returns 403 because it stops at checkAuth; validation never runs.

    checkAuth reads the Authorization request header. The expected value is:

    text
    Authorization: Bearer SI679

    This shared string is only a classroom example. A real application would not hard-code a credential in source code.

    validateBookParams requires title and author to be strings containing at least one non-space character. It returns 400 with a helpful message when the submitted book fails that check.

    Checkpoint: what happens if middleware neither sends a response nor calls next()? The request stays open and appears to hang.

    5. Walk through the API
    RequestAuth?Result
    POST /booksyesvalidate, create a book, return 201 and JSON
    GET /booksnoreturn all books or exact-match filters
    GET /books/:idnoreturn one book or 404
    PATCH /booksyesupdate selected fields, 400 for bad input, 404 if absent
    DELETE /booksyesremove the ID and return 200
    GET /books/badroutenothrow an example error handled as 500
    Query filtering

    GET /books checks title, author, and year. Every supplied filter must match, so author and year together act like an AND condition. Query values are strings; the code converts year to a number before comparing it with a book's numeric year.

    GET by ID

    Route parameters are strings. Number(req.params.id) converts the route value before the service compares it with a numeric book ID.

    POST status

    The assignment changes a successful POST from the default 200 to 201 Created. The response contains the created book, including its generated ID and default available status.

    PATCH validation

    The body must contain an id plus at least one field to change. The router separates them with:

    ts
    const { id, ...changes } = req.body;

    changes contains everything except id. The service applies those fields to the existing book with Object.assign().

    6. Understand the app/server split and error handler

    app.ts creates and exports the Express app. index.ts imports it and calls listen(). Supertest imports app.ts, so it can make requests without asking you to start the server first.

    The final error handler has four parameters:

    ts
    (err, req, res, next)

    Express uses that four-parameter shape to recognize error middleware. It is registered after the router because Express looks forward through the chain when a handler throws.

    Checkpoint: move the error handler above the router in a drawing. Why would an error thrown later fail to reach it?

    7. Read the tests as examples of API use

    Supertest builds a request with a readable chain:

    ts
    await request(app)
      .post('/books')
      .set('Authorization', 'Bearer SI679')
      .send({ title: 'The Hobbit', author: 'J.R.R. Tolkien', year: 1937 });

    The test then checks res.status, res.body for JSON, or res.text for a plain text response. The Week 2 tests cover:

    • empty initial state;
    • exact author filtering;
    • missing and incorrect tokens;
    • blank-title validation and its message;
    • POST creation and 201;
    • GET by ID and missing-ID 404;
    • PATCH auth, input validation, partial update, and unchanged fields;
    • DELETE auth and final empty state.

    beforeEach() calls _resetCatalog(). Without it, a book posted by one test would remain for the next test and make the outcome depend on test order.

    8. Suggested study session
    First 20 minutes: draw the architecture
    • Draw all five source-file layers.
    • Follow one POST request from app.ts through middleware and the service.
    • Follow the response back to the client.
    Next 25 minutes: middleware practice
    • Write the ordered chain for GET, POST, PATCH, and DELETE.
    • Predict which status wins for a request with both bad auth and bad data.
    • Use Postman to compare no token, the wrong token, and the correct token.
    Next 25 minutes: route behavior
    • POST two books by different authors.
    • Filter by one author, then by author plus year.
    • GET one returned ID.
    • PATCH its title and verify its author did not change.
    • DELETE it and confirm it is absent.
    Final 20 minutes: tests
    • Run npm test.
    • Read one test at a time using arrange, act, assert:

    set up data, send the request, check the result.

    • Temporarily remove _resetCatalog() and observe how shared state changes the

    suite. Restore it before finishing.

    Final self-check

    You are ready for Week 3 when you can answer these without opening the code:

    • 1. Why use a router instead of putting every route in index.ts?
    • 2. What are the two valid ways for middleware to finish its work?
    • 3. Why does middleware order change the response?
    • 4. What do 201, 400, 403, 404, and 500 mean here?
    • 5. Why are app.ts and index.ts separate?
    • 6. Why does every test reset the catalog?
    • 7. When should a test inspect res.body versus res.text?

    Useful commands:

    bash
    npm run typecheck
    npm test
    npm run build
    npm run dev
    HW1 · SliceDrop · complete original guide

    Original Markdown snapshot · Homework/si-679-f-26-hw1-kelvintiger/TEACHING_GUIDE.md

    SliceDrop HW1: A Beginner's Guide to the Finished API

    This guide explains the completed SliceDrop menu and orders API from the ground up. It assumes that HTTP, Express, TypeScript, and automated tests are all new. The examples and descriptions match the code and checked-in tests in this repository.

    The assignment is small on purpose. It gives one application enough pieces to show the Week 1 and Week 2 ideas working together:

    • a client sends HTTP requests;
    • an Express server receives them and runs middleware in order;
    • routers choose the behavior for a path;
    • plain functions validate data and look up menu items;
    • a small in-memory data module stores orders;
    • the server returns a status code, headers, and a JSON body;
    • Vitest and Supertest check the observable behavior.
    1. The mental model: a client talks to a server through an API

    An API is a set of agreed request and response shapes. A client can be a web page, a mobile app, Postman, curl, or a test. The client does not call TypeScript functions such as addOrder directly. It sends an HTTP request.

    text
    client
      │  HTTP request: method, URL, headers, optional JSON body
      ▼
    Express application (`app`)
      │  body parser → router → auth middleware → handler → data/validation
      ▼
    HTTP response: status, headers, JSON body
      │
      ▼
    client reads the result

    For example, a customer placing an order sends:

    http
    POST /orders HTTP/1.1
    Authorization: Bearer slicedrop-customer-secret
    Content-Type: application/json
    
    {"customerName":"Ada","items":[{"menuItemId":"soda","quantity":1}]}

    The server parses the JSON body, checks the token, validates the order, stores it in memory, and answers with 201 Created and a JSON representation of the new order. The client only needs the API contract: it does not need to know which array or function stored the order.

    This project has no database, browser UI, or deployed service. The API is the Express application. The index.ts file is the part that opens a network port when the application is run as a server.

    2. HTTP pieces used by this API
    Methods and routes

    An HTTP method says what kind of operation the client is requesting. A route is the method plus the path pattern. The same path can have different behavior for different methods; GET /orders and POST /orders are separate routes.

    Method and pathWho may call itBehavior
    GET /menuanyoneReturn all six menu items, or filter by category.
    GET /menu/:idanyoneReturn one menu item by its exact ID, or 404.
    POST /orderscustomer tokenValidate and create an order, or return validation errors.
    GET /ordersstaff tokenReturn all stored orders, or filter by status.

    The :id in GET /menu/:id is a route parameter. A request for /menu/hawaiian makes req.params.id equal to the string "hawaiian".

    Status codes

    The status code is a compact description of what happened.

    • 200 OK means the request succeeded. It is used for menu responses and

    successful order listings.

    • 201 Created means a new resource was created. A valid POST /orders

    returns this status.

    • 400 Bad Request means the client sent something the server cannot process,

    such as invalid JSON or an invalid order body.

    • 401 Unauthorized means the request did not present the required valid

    token. This assignment uses 401 for a missing, malformed, unknown, or wrong-role token.

    • 404 Not Found means no route handled the path, or a requested menu ID does

    not exist.

    • 500 Internal Server Error means application code failed unexpectedly. It

    is a server problem, even when it happens while handling a valid request.

    The status is part of the API contract. A client can make a useful decision from 201, 400, or 401 even before reading the body.

    Headers

    Headers are named metadata attached to an HTTP request or response. This API uses two request headers especially:

    • Authorization: Bearer <token> carries the customer or staff credential.
    • Content-Type: application/json tells the server that the body is JSON.

    express.json() uses the content type and JSON parser to turn a valid JSON request body into req.body. If JSON cannot be parsed, the parser raises an error before an order handler runs.

    Body, query string, and route parameters

    These are three different places request data can live:

    text
    GET /menu/hawaiian?category=pizza
        └──────┬─────┘ └──────┬──────┘
     route parameter `id`      query parameter `category`
    • The body is normally used for data being submitted. The order body contains

    customerName and items.

    • A query string begins after ?. GET /menu?category=pizza puts the value

    in req.query.category; GET /orders?status=pending puts the value in req.query.status. Query filters do not create a missing-resource error: an unknown category or status produces 200 with [].

    • A route parameter is part of the path pattern. In /menu/:id, the value is

    available at req.params.id.

    Week 1 introduced these ideas with small app.get and app.post handlers. HW1 applies the same ideas after the handlers have been organized into routers.

    3. The project map and the app/server split

    The important files fit together like this:

    text
    src/index.ts                 starts listening on port 3000
    src/app.ts                   builds and exports the Express app
    src/routers/menu.ts          GET /menu and GET /menu/:id
    src/routers/orders.ts        POST /orders and GET /orders
    src/middleware/auth.ts       Bearer parsing and role checks
    src/validation/validate-order.ts
                                  order-body validation
    src/data/menu.ts             supplied menu array and lookup function
    src/data/orders.ts           in-memory order array and ID generation
    src/types.ts                 shared TypeScript types
    src/constants.ts             tokens and port
    src/__tests__/*.test.ts      Vitest/Supertest behavior checks

    src/app.ts exports app but does not call listen. src/index.ts imports that app and starts the real server:

    ts
    import { app } from "./app";
    import { PORT } from "./constants";
    
    app.listen(PORT, () => {
      console.log(`SliceDrop listening on http://localhost:${PORT}`);
    });

    This split is important for testing. Supertest can import the app and manage a temporary listener for each test run, so the developer does not have to start the server on its normal fixed port first. Calling listen in app.ts would start that fixed listener whenever the module was imported and could cause port conflicts.

    The supplied constants.ts gives the exact development values:

    text
    customer token: slicedrop-customer-secret
    staff token:    slicedrop-staff-secret
    port:           3000

    These hard-coded tokens are suitable only for this exercise. The comments in that file point toward a later course topic: real secrets should come from configuration such as environment variables, rather than source code.

    4. The middleware chain: order is behavior

    Express processes a request by walking through the middleware and route handlers in the order they were registered. Each ordinary middleware has the shape (req, res, next):

    • inspect or change the request;
    • send a response and stop, or call next() to continue;
    • never do neither, or the request can hang.

    The finished app.ts registers the shared chain in this order:

    text
    1. express.json()
    2. /menu router
    3. /orders router
    4. JSON 404 handler
    5. four-argument error handler

    The order matters in several ways.

    JSON parsing must be before routes

    The order handlers expect req.body to contain parsed JSON. Therefore express.json() must be registered before both routers. A valid JSON body is available to the order handler; malformed JSON causes the parser to pass an error to the final error handler.

    Authorization must be before order validation

    POST /orders has this route-specific chain:

    text
    express.json()
      → orders router matches `/`
      → requireCustomerToken
      → POST handler
      → validateOrder
      → addOrder

    The customer middleware is placed before the handler, so a request with no token gets 401 without being validated. The test named “auth runs before validation” deliberately sends {} with no token and expects 401, not 400. That result is a consequence of registration order, not a special case inside the validator.

    GET /orders follows the same pattern with requireStaffToken before the listing handler. A valid customer token is still rejected there because the route requires the staff token.

    404 must be after the routers

    The 404 handler is a normal fall-through middleware. If it came before the routers, it would answer every request and the routers would never run. After the routers, it means “no earlier route matched.” It returns:

    json
    {"error":"Not found"}
    The error handler must be last and have four parameters

    Express identifies error-handling middleware by the exact four-parameter form:

    ts
    (err, req, res, next)

    The finished handler is registered after the 404 handler. The next parameter is not used in this assignment, but it must remain in the function signature so Express recognizes the function as an error handler.

    5. Authentication and exact Bearer parsing

    The authentication module separates token parsing from Express middleware. That is a useful Week 2 design choice: the parser is a plain function that can be tested without constructing a request, while the middleware handles the HTTP response and next().

    What counts as a valid header

    parseBearerToken(header) accepts only the exact shape:

    text
    Bearer <one-or-more-non-whitespace-characters>

    The implementation uses the case-sensitive regular expression /^Bearer ([^\s]+)$/.

    Header valueResult
    missing/undefinednull
    Bearer slicedrop-customer-secrettoken string
    bearer slicedrop-customer-secretnull because Bearer is case-sensitive
    Basic slicedrop-customer-secretnull
    Bearer null because the token is empty
    Bearer one twonull because the token contains whitespace
    Bearer nope"nope"; parsing succeeds, but authorization rejects it

    The parser does not decide whether the token is customer or staff. It only extracts a candidate string or returns null.

    What the two middleware functions do

    requireCustomerToken reads req.headers.authorization, passes it to the parser, and compares the result to CUSTOMER_TOKEN. If it does not match, it sends:

    http
    401 Unauthorized
    json
    {"error":"Unauthorized"}

    On a match it calls next(), allowing validation and order creation to run. requireStaffToken has the same structure but compares with STAFF_TOKEN.

    The distinction is intentional: possession of a valid customer token does not grant access to the staff-only order list.

    6. The menu API
    GET /menu

    With no query parameter, the menu router returns the supplied menu array directly as JSON. It contains six items:

    text
    hawaiian, meat-lovers, build-your-own,
    bread-nugz, dipping-sauce, soda

    With ?category=pizza, the handler uses menu.filter(...) and keeps items whose category exactly equals "pizza". The result contains three pizzas.

    http
    GET /menu?category=pizza
    json
    [
      {"id":"hawaiian","name":"Hawaiian","category":"pizza", "...":"..."},
      {"id":"meat-lovers","name":"Meat Lovers","category":"pizza", "...":"..."},
      {"id":"build-your-own","name":"Build Your Own","category":"pizza", "...":"..."}
    ]

    The "..." markers above mean additional real fields are present; they are not literal response fields. A category with no matches, such as category=seafood, returns 200 and []. If a query value is not a string (for example, a repeated or unusually shaped query parameter), the handler also returns [] rather than treating it as an exact category.

    GET /menu/:id

    The handler reads req.params.id and calls findMenuItem(id). That supplied function uses menu.find(...) and returns either a MenuItem or undefined.

    For a known ID:

    http
    GET /menu/hawaiian

    the response is 200 and includes the full item, including its three sizes, crust choices, and toppings. For an unknown ID:

    http
    GET /menu/sushi

    the response is:

    http
    404 Not Found
    json
    {"error":"Menu item not found"}

    Returning [] would be appropriate for an empty collection filter, but a single requested resource that does not exist is represented by 404.

    7. Orders: creating and listing data
    POST /orders

    The request first needs the customer token. Once authorized, the handler calls validateOrder(req.body). An empty error list means the body is valid. A non-empty list is returned as:

    http
    400 Bad Request
    json
    {
      "errors": [
        "customerName must be a non-blank string",
        "items must be a non-empty array"
      ]
    }

    For valid input, addOrder(req.body) creates and stores the order, and the handler returns 201:

    http
    POST /orders
    Authorization: Bearer slicedrop-customer-secret
    Content-Type: application/json
    json
    {
      "customerName": "Ada",
      "items": [
        {"menuItemId": "hawaiian", "quantity": 2, "size": "large"}
      ]
    }
    http
    201 Created
    json
    {
      "id": "1",
      "status": "pending",
      "createdAt": "2026-09-16T12:00:00.000Z",
      "customerName": "Ada",
      "items": [
        {"menuItemId": "hawaiian", "quantity": 2, "size": "large"}
      ]
    }

    The timestamp in a real response is the current time, so tests check that it is a parseable string instead of comparing one fixed value. IDs are strings and increase from "1"; each call receives a distinct ID while the process is running. Every new order starts with status "pending".

    GET /orders

    This endpoint requires the staff token:

    http
    GET /orders
    Authorization: Bearer slicedrop-staff-secret

    With no filter it returns the current array in insertion order. With ?status=pending, it calls listOrders("pending") and returns only matching orders. An unknown or currently empty status returns 200 and []; it is not an invalid request.

    The order data is deliberately in memory. src/data/orders.ts starts with:

    ts
    let orders: Order[] = [];
    let nextId = 1;

    addOrder creates an Order, increments nextId, pushes the order, and returns it. listOrders either returns all orders or uses filter for a status. _resetOrders() clears the array and resets IDs for tests. The orders are lost and IDs start over when the Node process restarts; this is not a persistent database.

    8. Order validation, including every error

    validateOrder(body) is a plain function returning string[]. It does not send an HTTP response. The route handler decides that a non-empty array means 400. Keeping validation separate makes the rules easier to read and test.

    The function is defensive because req.body comes from an outside client. A client can send null, a number, an array, or an object with unexpected field types. The implementation checks before reading nested properties.

    Request-level checks
    • 1. It checks whether body is a non-null object and not an array. If not, the

    values used for validation are treated as undefined.

    • 2. customerName must be a string whose trim() is not empty. This rejects a

    missing value, a number, "", and whitespace-only text.

    • 3. items must be a non-empty array. If it is not an array, the function

    returns the errors found so far rather than calling .forEach on an unsafe value.

    Item-level checks

    For every item, the validator checks:

    • the item is object-like before reading its fields;
    • menuItemId is a string identifying an existing menu item;
    • quantity is an integer at least 1;
    • an optional size is offered by that menu item;
    • an optional crust is offered by that menu item;
    • an optional sauce is offered by that menu item;
    • an optional toppings value is an array, and every topping name is offered.

    The validator uses the item index in messages, such as items[0].quantity must be a whole number of at least 1. It does not stop at the first problem. For example, an order with a blank name, an unknown menu ID, and quantity 0 receives all applicable messages in the same errors array. This is more useful to a client than forcing three separate requests.

    Option checks happen only when the menu item was found. That avoids trying to read option lists from undefined. It also means an unknown menu item gets the menu-ID error without a misleading size, crust, sauce, or topping error.

    The four supplied helper functions at the bottom of the file implement the membership checks:

    • offersSize returns false when sizes is absent, then uses .some to

    compare an option's name.

    • offersCrust returns false when crusts is absent, then uses .includes.
    • offersSauce does the same for sauces.
    • offersTopping returns false when toppings is absent, then uses .some

    to compare topping names.

    Matching is exact and case-sensitive. "Large" is not the same as "large". The soda item has sizes but no crusts or toppings; a supplied unsupported option therefore fails validation.

    9. TypeScript ideas in this code
    Interfaces and type aliases

    src/types.ts describes the shapes shared across modules.

    • Category is a union of four allowed strings: "pizza", "appetizer",

    "side", and "beverage".

    • SizeOption and ToppingOption require a name and numeric price.
    • MenuItem requires id, name, and category; description, price,

    sizes, crusts, sauces, and toppings are optional with ?.

    • OrderStatus is the union "pending" | "in-progress" | "completed".
    • OrderItem requires menuItemId and quantity, while customization fields

    are optional.

    • NewOrder is the customer-submitted shape.
    • Order extends NewOrder, so an Order has all of the new-order fields plus

    id, status, and createdAt.

    An interface describes an object shape; a type alias such as Category or OrderStatus can describe a set of allowed literal values. TypeScript checks these shapes while compiling, but a client can still send bad runtime data. That is why validateOrder still performs real runtime checks.

    undefined and narrowing

    Optional properties may be absent, so TypeScript represents their values as possibly undefined. findMenuItem returns MenuItem | undefined because a lookup may fail. The code narrows before using the value:

    ts
    const menuItem = findMenuItem(id);
    if (menuItem === undefined) {
      res.status(404).json({ error: "Menu item not found" });
      return;
    }
    
    res.json(menuItem); // here TypeScript knows it is a MenuItem

    The validator uses checks such as Array.isArray(items), typeof category === "string", and menuItem === undefined to narrow broad runtime values into safe, more specific ones. The early return statements make those narrowed facts easy to follow.

    validateOrder(body: any) intentionally accepts any incoming body so it can inspect malformed values. any turns off compile-time protection for that value, so the function compensates with explicit runtime guards. The topping callback uses unknown, which is safer: a value of type unknown must be checked before treating it as a string.

    Arrays and callback methods

    Several small array methods express the business rules directly:

    ts
    menu.find((item) => item.id === id);       // one matching item or undefined
    menu.filter((item) => item.category === category); // all matches
    items.forEach((item, index) => { ... });   // validate every item
    options.some((option) => option.name === size); // any option matches
    crusts.includes(crust);                    // exact string membership

    The tests also use .map to project order names and .every to verify that a filtered result satisfies a condition. These methods take callback functions: the arrow function receives an element, and the method returns the appropriate result without requiring a manual index loop.

    10. File-by-file walkthrough of the completed work

    This section follows the changed files from the outside of the application toward the inside.

    src/app.ts
    • 1. The imports bring in Express request/response types, the two routers, and

    the types needed by the error handler.

    • 2. export const app = express() creates the application object and exports it

    for both index.ts and tests.

    • 3. app.use(express.json()) installs the body parser before any route.
    • 4. app.use("/menu", menuRouter) mounts the menu router at the /menu base

    path. app.use("/orders", ordersRouter) does the same for orders.

    • 5. The next middleware returns JSON 404 for any request that got through

    both routers without a match.

    • 6. The final four-argument middleware distinguishes malformed JSON from other

    errors. A SyntaxError with a body property is answered with 400 and { error: "Request body must be valid JSON" }. Any other error is logged with console.error and answered with 500 and { error: "Internal server error" }.

    The return statements after responses are control-flow guards. They stop the handler from trying to send a second response.

    src/middleware/auth.ts
    • 1. It imports Express types and the two token constants.
    • 2. parseBearerToken handles only extraction. It returns null for a missing

    header or a regex mismatch and returns capture group 1 for a valid header.

    • 3. requireCustomerToken calls the parser, sends 401 on any non-matching

    token, and calls next() only for the customer token.

    • 4. requireStaffToken repeats that pattern for the staff token.

    The parser is deliberately not middleware. A plain function has no response to send and no next() to call, so its job stays focused and direct.

    src/routers/menu.ts
    • 1. It imports Express request/response/router types and the supplied menu data

    functions.

    • 2. menuRouter = Router() creates a router whose paths are relative to the

    mount point in app.ts.

    • 3. menuRouter.get("/", ...) handles the mounted GET /menu. It returns the

    whole array when category is absent, returns [] when the query value is not a string, and otherwise filters for an exact category.

    • 4. menuRouter.get("/:id", ...) handles mounted GET /menu/:id. It looks up

    the ID, returns a 404 error object when there is no item, and returns the item when there is one.

    Because the router is mounted at /menu, its internal / is not just / to the client. The combination is /menu/ (and Express also handles the usual slash variation), while its internal /:id becomes /menu/:id.

    src/routers/orders.ts
    • 1. It imports the supplied addOrder and listOrders functions, both auth

    middleware functions, and validateOrder.

    • 2. ordersRouter = Router() creates a relative router.
    • 3. ordersRouter.post("/", requireCustomerToken, handler) puts customer auth

    before the handler. The handler collects validation errors, returns 400 with { errors } when needed, and otherwise calls addOrder and returns 201 with the new order.

    • 4. ordersRouter.get("/", requireStaffToken, handler) puts staff auth before

    the listing logic. The handler returns all orders without a status query, returns [] for a non-string query value, and passes a string status to listOrders.

    The leading slash in each router path is relative to the /orders mount, so the public paths are POST /orders and GET /orders.

    src/validation/validate-order.ts
    • 1. It imports the MenuItem type and findMenuItem.
    • 2. It creates an error accumulator, checks whether the body is a non-array

    object, and validates customerName.

    • 3. It validates that items is a non-empty array. If not, it returns the

    accumulated errors safely.

    • 4. It loops through every item, looks up the menu ID, checks quantity, and

    returns the menu-ID and quantity errors as appropriate.

    • 5. When an item and menu item are both safe to inspect, it checks each optional

    customization by calling the supplied helper functions.

    • 6. It returns the complete errors array. An empty array is the success signal.
    • 7. The four helpers at the bottom handle option membership and safely treat an

    absent option list as “not offered.”

    The function deliberately accumulates with errors.push(...) instead of returning immediately from the first bad field. It only returns early when continuing would be unsafe, such as when items is not an array.

    src/types.ts

    This supplied module is the shared vocabulary. It prevents a menu item from silently becoming an arbitrary object inside the typed parts of the program, documents which fields are optional, and distinguishes a customer-submitted NewOrder from a stored Order with server-generated fields.

    src/data/menu.ts

    This supplied module defines the six menu items and reusable constants for standard toppings, crusts, and dipping sauces. findMenuItem uses .find and can return undefined. The data is process-local and is not changed by the API routes.

    src/data/orders.ts

    This supplied module is the exercise's temporary database. addOrder creates the server fields, increments the ID counter, pushes into the array, and returns the object. listOrders preserves insertion order when unfiltered and uses .filter for a status. _resetOrders is test-only and is never called by application routes.

    src/constants.ts and src/index.ts

    constants.ts holds the two tokens and port. index.ts is the executable entrypoint that starts listening. Keeping this start-up action out of app.ts is what lets Supertest import the app safely.

    11. How the tests map to requirements

    Vitest supplies describe, it, beforeEach, expect, and test doubles such as vi. Supertest supplies a client-like API:

    ts
    const res = await request(app)
      .post("/orders")
      .set("Authorization", `Bearer ${CUSTOMER_TOKEN}`)
      .send(ORDER);
    
    expect(res.status).toBe(201);
    expect(res.body.status).toBe("pending");

    request(app) gives the exported Express app to Supertest. It does not require the developer to run npm run dev or start port 3000; Supertest manages the temporary listener used for the request. .set adds a header, .send supplies a JSON body, and Supertest parses JSON responses into res.body.

    src/__tests__/auth.test.ts
    • verifies that missing and unknown customer tokens produce 401;
    • verifies that the customer token allows POST /orders;
    • verifies that missing, unknown, and customer tokens are rejected by

    GET /orders;

    • verifies that the staff token allows the order list;
    • verifies that authentication runs before validation by expecting 401 for a

    missing token and bad body.

    src/__tests__/errors.test.ts
    • verifies a path no router handles returns 404 with an error field;
    • verifies invalid orders return 400 with an errors array;
    • verifies the validator catches unknown menu IDs, quantities below one,

    unsupported crusts, and unsupported sizes;

    • mocks addOrder to throw and verifies an application failure becomes 500,

    not a client-facing 400;

    • sends malformed JSON and verifies the parser error becomes a JSON 400.

    The mock test is especially useful because it distinguishes two error sources: the client sent valid JSON and a valid order, but application code still failed.

    src/__tests__/menu.test.ts
    • checks that the unfiltered menu has six items;
    • checks exact category filtering for pizzas;
    • checks an unknown category returns an empty successful array;
    • checks a known item includes customization options;
    • checks an unknown ID returns 404 with an error field.
    src/__tests__/orders.test.ts
    • checks successful creation returns 201, preserves customer and items,

    starts as pending, gives an ID, and creates an ISO-parseable timestamp;

    • checks two orders receive different IDs;
    • checks an empty order list before creation;
    • checks status filtering and the empty result for a status with no matches;
    • checks that unfiltered results preserve insertion order.

    Each order test file calls _resetOrders() in beforeEach. That keeps tests independent even though the application data module is stateful. Tests should not rely on another test having run first.

    The timestamp assertion demonstrates a general testing rule: assert a stable property, such as “is a parseable date string,” rather than an unstable exact value, such as the clock time from one particular run.

    12. Install, run, test, type-check, and build

    Run these commands from the repository directory:

    bash
    npm install
    npm test
    npm run typecheck
    npm run build

    What each command does:

    • npm install installs the dependencies in package.json and records them

    through the lockfile.

    • npm test runs vitest run, which executes the test suite once.
    • npm run test:watch runs Vitest interactively and reruns tests as files

    change.

    • npm run typecheck runs tsc --noEmit. It checks types without creating

    output files.

    • npm run build runs tsc -p tsconfig.build.json and emits compiled server

    code into dist while excluding tests.

    • npm run dev runs tsx src/index.ts, starts the server on port 3000, and

    prints its local URL. tsx runs TypeScript directly; it does not replace the separate type-check command.

    With the development server running, equivalent manual requests include:

    bash
    curl http://localhost:3000/menu
    curl 'http://localhost:3000/menu?category=pizza'
    curl http://localhost:3000/menu/hawaiian
    
    curl -X POST http://localhost:3000/orders \
      -H 'Authorization: Bearer slicedrop-customer-secret' \
      -H 'Content-Type: application/json' \
      -d '{"customerName":"Ada","items":[{"menuItemId":"soda","quantity":1}]}'
    
    curl http://localhost:3000/orders \
      -H 'Authorization: Bearer slicedrop-staff-secret'

    The last two requests must occur in the same running process if the listing is expected to show the newly created order. Restarting the process clears the in-memory order array.

    The repository README says the short workflow is to install dependencies and make the tests pass, and that the live assignment specification is the primary source when available. The checked-in tests are the concrete, executable examples of the behavior described here.

    13. Common mistakes and a debugging routine
    Common implementation mistakes
    • 1. Registering express.json() too late. Put it before both routers so

    req.body is ready.

    • 2. Putting auth inside validation or after the handler. Auth belongs as the

    route-specific middleware argument before the handler. Otherwise a missing token can incorrectly produce 400 or create an order.

    • 3. Using the wrong token for a role. The customer token is valid only for

    POST /orders; the staff token is required for GET /orders.

    • 4. Accepting loose Bearer formats. The required parser is exact: capital

    Bearer, one literal separating space, and a non-whitespace token with no extra text.

    • 5. Mounting paths twice. Once the app mounts menuRouter at /menu, the

    router should define / and /:id, not /menu and /menu/:id.

    • 6. Returning 200 for a missing single item. Unknown /menu/:id values

    are 404; unknown collection filters are 200 with [].

    • 7. Stopping validation at the first error. Keep pushing all discovered

    problems into the array.

    • 8. Calling .forEach before checking items. Check Array.isArray(items)

    first. Likewise, check that a menu lookup succeeded before reading option lists.

    • 9. Confusing malformed JSON with an invalid order. Malformed JSON is a

    parser error and gets 400 from the global handler. Valid JSON with bad fields gets 400 with { errors: [...] }. A thrown application exception gets 500.

    • 10. Forgetting to return after sending. A handler that sends an error and

    keeps running can attempt a second response and trigger another error.

    • 11. Removing the fourth error-handler parameter. Express uses the four

    parameters to recognize error middleware, even when next is unused.

    • 12. Expecting orders to survive a restart. The array is in memory. Restart

    means an empty list and IDs beginning again at "1".

    • 13. Testing an exact timestamp. Check its type and parseability, as the

    supplied test does.

    • 14. Relying on test order. Reset state in beforeEach; a test should be

    able to run by itself.

    A practical debugging routine

    When a request fails, inspect the problem in this order:

    • 1. Confirm the method and full path. POST /orders and GET /orders are

    different, and a router path is relative to its mount point.

    • 2. Check the status code before reading the body. 401 points to auth, 404

    to routing or an unknown menu ID, and 400 to parsing or validation.

    • 3. Check the headers. For orders, use the exact token and

    Content-Type: application/json.

    • 4. Check the body shape. customerName must be non-blank and items must be

    a non-empty array; each item needs a known menu ID and integer quantity.

    • 5. Add a focused test or run one existing test by its description. A failing

    test tells you the observable contract rather than an internal guess.

    • 6. If the status is 500, read the server log. The global handler logs the

    unexpected error while returning a deliberately generic client message.

    • 7. Run npm run typecheck after fixing behavior. Passing tests alone does not

    prove that TypeScript's compile-time checks pass.

    When the result seems surprising, trace the chain from the top: JSON parser, mount point, router path, route-specific middleware, handler, data function, then response. That follows the order Express actually uses.

    14. Self-check quiz

    Try to answer these without looking at the answer key.

    • 1. What is the difference between the exported app and the server started by

    index.ts?

    • 2. Which middleware makes a valid JSON body available as req.body?
    • 3. Why must that middleware be registered before the routers?
    • 4. What public path is produced when app.ts mounts menuRouter at /menu

    and the router defines get("/:id", ...)?

    • 5. Which token and method are required to create an order?
    • 6. What does parseBearerToken("Bearer nope") return, and where is the token

    accepted or rejected afterward?

    • 7. What response should a request with no token and body {} receive for

    POST /orders?

    • 8. What is the difference between an unknown category and an unknown menu ID?
    • 9. Why does validateOrder return an array instead of immediately responding?
    • 10. Name three conditions that make quantity invalid.
    • 11. Why does the validator check menuItem === undefined before checking a

    size or topping?

    • 12. What does Order extends NewOrder communicate?
    • 13. Why does the error handler need four parameters if it does not call

    next()?

    • 14. How does the application distinguish malformed JSON from an application

    exception?

    • 15. What happens to orders when the Node process restarts?
    • 16. Why do order tests call _resetOrders() before each test?
    • 17. Which command runs tests once, and which command checks types without

    emitting JavaScript?

    • 18. What status should a successful order creation return, and why is that more

    specific than 200?

    Answer key
    • 1. app.ts constructs and exports the Express app for reuse; index.ts

    imports it and calls listen on port 3000.

    • 2. express.json().
    • 3. It must parse the body before handlers that read req.body; its position in

    the chain controls whether the routes see parsed data.

    • 4. GET /menu/:id, such as GET /menu/hawaiian.
    • 5. POST /orders requires the exact customer token

    slicedrop-customer-secret in Authorization: Bearer ... form.

    • 6. It returns the string "nope"; the customer or staff middleware compares

    that candidate with its required constant and rejects it with 401.

    • 7. 401 Unauthorized, because auth runs before validation.
    • 8. An unknown category is an empty successful collection (200, []); an

    unknown single menu ID is 404 with an error object.

    • 9. A plain function can be tested and reused independently of Express; the

    route handler translates its returned errors into an HTTP response.

    • 10. It is missing, not a number, not an integer, zero, or negative. Fractions

    are also invalid.

    • 11. A failed lookup returns undefined, so reading option properties would be

    unsafe and could cause a server exception.

    • 12. A stored Order has all NewOrder fields plus server-generated id,

    status, and createdAt.

    • 13. Express recognizes error middleware by (err, req, res, next); the fourth

    parameter is part of that recognition rule.

    • 14. The handler checks for SyntaxError with a body property and returns

    400; all other errors are logged and return generic 500.

    • 15. The in-memory array disappears, so orders are lost and the next ID starts

    at "1" in the new process.

    • 16. To clear shared in-memory state and keep tests independent of execution

    order.

    • 17. npm test runs the suite once; npm run typecheck runs tsc --noEmit.
    • 18. 201 Created, because the request created a new order resource and the

    status communicates that fact to the client.

    If the answer to any question was uncertain, trace the matching request through the middleware chain and then read the corresponding test. That is the same method used to understand the rest of this API: identify the request, follow the ordered handlers, and inspect the status and JSON response.

    Week 3 · complete original guide

    Original Markdown snapshot · Classes/si-679-f-26-week03-nyt-kelvintiger/STUDY_PLAN.md

    Week 3 Study Plan: MongoDB, Express, and API Tests

    Use this guide with the completed code in src/. It starts with the ideas behind the code, then walks through each file and gives you small checks to make sure you can explain what is happening.

    What you should be able to do

    By the end of this week, you should be able to:

    • 1. explain how a MongoDB collection and document compare with a SQL table and row;
    • 2. connect a TypeScript program to a MongoDB database;
    • 3. create, read, update, and delete documents;
    • 4. place database functions behind Express routes;
    • 5. recognize a MongoDB ObjectId and reject a malformed one;
    • 6. use Vitest, Supertest, and MongoMemoryServer to test an API without changing a real database;
    • 7. use test hooks to give every test a clean starting state.
    1. Start with the database vocabulary

    MongoDB is a document-oriented NoSQL database. A useful first comparison is:

    MongoDBRough SQL comparisonExample in this project
    databasedatabaseweek3app or the test database
    collectiontableproducts
    documentrowone Duct Tape product
    fieldcolumnname, price, or quantity

    A product document looks much like a JavaScript object:

    ts
    {
      name: 'Duct Tape',
      price: 5.99,
      quantity: 120
    }

    MongoDB adds an _id field when the document is inserted. The value is an ObjectId. Its printed form is a string of 24 hexadecimal characters, but the database value is an ObjectId, not an ordinary string.

    Checkpoint:

    • Which collection stores the records in this app?
    • Which part uniquely identifies one product?
    • Why must new ObjectId(id) be used before searching by _id?
    2. Practice Mongo operations directly

    Read src/db-explore.ts first. It is a scratch program for learning the database driver before Express is involved.

    The connection sequence is:

    text
    MongoClient(uri) -> client.connect() -> client.db(name) -> collection(name)

    The file includes these four CRUD operations:

    GoalMongoDB method
    create documentsinsertMany()
    read documentsfind(), findOne()
    update documentsupdateOne(), updateMany()
    delete documentsdeleteOne()

    Mongo queries are objects. { name: 'Masking Tape' } means “documents whose name equals Masking Tape.” { price: { $gte: 5 } } means “documents whose price is greater than or equal to 5.” The $set update operator changes only the fields named inside it.

    The four Now You Try functions demonstrate those ideas:

    • findPricierTest() uses $gte to find products costing at least $5.
    • updatePriceTest() changes only Scotch Tape's price.
    • deleteByNameTest() deletes a document using a name query.
    • markOnSaleTest() uses updateMany() to add onSale: true to every

    product costing less than $5.

    Exercise:

    • 1. Start local MongoDB.
    • 2. Uncomment await addProducts() in main() and run npm run explore once.
    • 3. Comment it again so repeated saves do not create duplicates.
    • 4. Run each Now You Try function one at a time and print the result.
    • 5. After each operation, print getAllProducts() and predict the output before

    looking at it.

    3. Separate database work from HTTP work

    src/db.ts is the database layer used by the application. It keeps the MongoDB driver details in one place. Express routes do not need to know how a client is created or which collection method performs an update.

    The public functions form a small interface:

    FunctionInputOutput
    connectURI and database namean open connection
    getAllProductsnoneevery product document
    getProductstring IDone document or null
    addProducta Productthe new ObjectId
    updateProductID and partial changesnumber of matched documents
    deleteProductIDnumber of deleted documents

    ProductUpdate makes each property optional. That matters because a PATCH request can change only price while leaving name and quantity alone.

    _clearProducts() is intentionally marked as a test helper. Calling it in the running app would erase the whole products collection.

    Checkpoint:

    • Why does updateProduct() return matchedCount instead of the full driver result?
    • How does a route use matchedCount === 0 to choose an HTTP status?
    • Why does connect() accept its URI and database name as arguments?
    4. Map CRUD operations to HTTP routes

    src/product-router.ts maps the database functions to an HTTP API:

    RequestDatabase actionSuccessful response
    GET /productsread all200 and a JSON array
    GET /products/:idread one200 and one JSON document
    POST /productscreate201 and { "id": ... }
    PATCH /products/:idupdate selected fields200
    DELETE /products/:iddelete one200

    A valid-looking ID that is absent from the database produces 404 Not Found. A malformed ID produces 400 Bad Request. These are different failures: the first identifies no document, while the second is not a usable MongoDB ID.

    src/validate-id.ts handles malformed IDs before a route calls the database. Without that middleware, new ObjectId('badID123') throws and looks like a server failure. ObjectId.isValid() lets the app report the client's input error accurately.

    src/app.ts first registers express.json(), then mounts the router at /products. The parser must run first so a POST or PATCH handler can read req.body.

    src/index.ts opens the real database connection before the server listens. Tests import app.ts instead, connect to their own temporary database, and do not run index.ts. This is why the app and server are split into two files.

    Exercise:

    • 1. Trace PATCH /products/abc... from app.ts to the response.
    • 2. Write down each function it passes through.
    • 3. Repeat with a malformed ID and notice where the path stops.
    5. Understand the test setup

    The tests use three tools with different jobs:

    • Vitest runs test files and provides describe, it, expect, and hooks.
    • Supertest sends HTTP requests directly to the Express app.
    • MongoMemoryServer starts a temporary real MongoDB process for the tests.

    The hooks establish a predictable lifecycle:

    text
    beforeAll:  start temporary MongoDB and connect once
    beforeEach: empty the products collection
    test:       arrange data, send request, check response and state
    afterAll:   disconnect and stop temporary MongoDB

    Cleaning before every test prevents one test's documents from changing a later test's result. Stopping MongoDB in afterAll prevents the test process from hanging.

    The main matchers in this project are:

    • toBe() for exact primitive values such as a status code;
    • toEqual() for arrays or objects with the same contents;
    • toMatch() for the 24-character ID pattern;
    • toHaveLength() for array size;
    • toContain() and .not.toContain() for membership.

    The PATCH test checks more than the changed price. It also checks that name and quantity stayed the same. That is what proves the code performs a partial update.

    The DELETE test verifies both the response and the database state by trying to read the deleted product and by checking the final product list.

    6. Suggested study session
    First 20 minutes: vocabulary and data flow
    • Draw the path from an HTTP request to MongoDB and back.
    • Label the app, router, database layer, collection, and response.
    • Explain collection, document, field, and ObjectId aloud.
    Next 25 minutes: direct Mongo practice
    • Work through db-explore.ts one function at a time.
    • Predict what each query matches.
    • Verify the resulting documents in getAllProducts() or MongoDB Compass.
    Next 25 minutes: API code
    • Read db.ts, then product-router.ts, then app.ts, then index.ts.
    • For each route, identify its input, database function, success status, and

    missing-resource behavior.

    Final 20 minutes: tests
    • Run npm test.
    • Temporarily change one expected status to the wrong value and read the

    failure. Restore it afterward.

    • Explain why every hook is beforeAll, beforeEach, or afterAll.
    • Add one extra GET-by-ID success test on your own.
    Final self-check

    You are ready to move on when you can answer these without opening the code:

    • 1. What is the difference between a MongoDB document and a JavaScript object?
    • 2. Why is a string ID converted to an ObjectId?
    • 3. What do $gte and $set do?
    • 4. Why does PATCH use optional fields?
    • 5. Why do malformed and missing IDs have different status codes?
    • 6. Why does each test need a clean collection?
    • 7. Why does the test suite import app instead of index?

    Useful commands:

    bash
    npm run typecheck
    npm run build
    npm test
    npm run explore
    npm run dev
    Week 4 · complete original guide

    Original Markdown snapshot · Classes/si-679-f-26-week-04-nyt-kelvintiger/STUDY_PLAN.md

    Week 4: REST APIs, layers, and tests

    Start with this: a client asks for something, the server does the work, and the server sends an answer. In this project, the thing is a product stored in MongoDB. The client could be Postman, a browser app, or one of our tests.

    The lecture and Now You Try instructions are the source for this exercise. We built the lecture's GET and POST routes, GET by id for Now You Try #1, its tests for #2, and both DELETE stretch items.

    1. What an API request contains

    An API is an agreed way for one program to ask another program to do something. Here the agreement uses HTTP. A request has a method and a path, and sometimes a body. For example:

    text
    GET /products

    GET means read. /products names the collection of products. The server answers with a status code and a JSON body. JSON is text representing objects and arrays. It looks like JavaScript, but keys must have double quotes.

    json
    [
      {
        "id": "507f1f77bcf86cd799439011",
        "modelName": "Coffee maker",
        "modelNumber": "CM1",
        "manufacturer": "Acme",
        "color": "Black",
        "price": 49.99,
        "quantity": 10
      }
    ]

    The outer square brackets mean an array. Each object is one product. That id is an illustration; use an id from your own response when making a request.

    2. What REST adds

    REST gives us principles for keeping the client/server agreement consistent. Use resource names such as /products and let the method describe the action.

    Method and pathMeaningSuccess code
    GET /productsRead the collection200
    GET /products/:idRead one product200
    POST /productsCreate a product201
    DELETE /products/:idRemove a product204

    :id is a placeholder in our route definition. A real client replaces it with the product's id. GET /products/507f1f77bcf86cd799439011 is a concrete request.

    200 means success with an answer. 201 means created. 204 means success with no response body. 400 means the request is malformed. 404 means the resource or route does not exist. 500 means an unexpected server failure.

    REST also says requests should be stateless: the request supplies what is needed to handle it. The server does not need to remember which product you asked for last time. This does not mean the database cannot store products between requests.

    The client receives a representation of a product: a JSON copy of its current fields. It does not receive direct control of the database document. If it changes its local copy, the stored product does not change.

    The lecture's six constraints are a uniform interface, client/server separation, stateless requests, cacheability, a layered system, and optional code on demand. Our exercise concentrates on naming, HTTP methods, representations, and layers. It does not implement every feature of a complete shop API.

    3. Why split the code into layers?

    We could put a database query inside every route. It works initially, but then database details, HTTP details, and application decisions are mixed together. This week separates those jobs:

    text
    client request
        -> app -> route -> controller -> service -> database
                                 <- product <- document
    client response <- controller

    The model helps the service turn the document into a product. These are folders inside one server program, not separate servers.

    FileIts job
    src/index.tsConnect to MongoDB, then start listening
    src/app.tsAssemble Express and middleware
    src/routes/product-routes.tsMatch methods/paths to controllers
    src/controllers/product-controllers.tsRead the request and send the HTTP response
    src/services/product-service.tsCoordinate product operations and conversion
    src/models/product.tsDescribe the Product type and convert fields/documents
    src/db/db.tsConnect and run MongoDB operations
    src/middleware/validate-id.tsCheck the path id before using it
    src/middleware/validate-product.tsCheck supplied product fields
    src/middleware/error-handler.tsSend a response for unexpected errors
    src/__tests__/products.test.tsCheck behavior against a temporary database

    The route knows about the controller. The controller knows about the service. The service knows about models and the database. The database does not know about Express. That direction matters: changing the HTTP response should not require changing a Mongo query.

    4. Follow GET for one product

    Open product-routes.ts. This line registers the route:

    ts
    productRouter.get('/:id', validateId, productControllers.getProduct);

    app.ts mounts this router at /products, so the complete path is /products/:id. validateId runs first. A Mongo ObjectId is represented here by 24 hexadecimal characters (digits and letters a–f). An invalid shape receives 400.

    The controller reads req.params.id. params is where Express puts values matched by path placeholders. The controller calls:

    ts
    const product = await productService.get(String(req.params.id));

    The service calls db.getInCollection(db.PRODUCTS, id). The database converts the string to an ObjectId and searches for a document whose _id matches:

    ts
    findOne({ _id: new ObjectId(id) })

    If nothing matches, Mongo returns null. The service returns null too. The controller turns that into 404. The service does not choose 404, because that is an HTTP decision.

    If a document exists, productFromDocument creates a new object containing only the product fields. Mongo's _id becomes a string named id. The controller calls res.json(product), which sends that object with status 200.

    5. Read the TypeScript without getting stuck
    ts
    const get = async (id: string): Promise<Product | null> => {

    Read it as: "get takes a string id, does asynchronous work, and eventually gives back a Product or null."

    • const declares a name that cannot be reassigned.
    • async means the function returns a Promise: an eventual result.
    • await waits for that eventual result before continuing this function.
    • Product | null means either a Product or no matching product.
    • Product[] means an array of Products.
    • Promise<void> means asynchronous work with no useful return value.

    type Product describes an object's fields for TypeScript. It helps the compiler catch mistakes; it does not check incoming JSON at runtime. Middleware does that. import type brings in a type for the compiler. Normal import brings in code that runs. Imports end in .js because that is the extension after compilation.

    Partial<Product> makes every Product field optional. The lecture uses it for incoming fields. fields.price ?? 0 supplies zero when price is missing, while preserving a supplied zero. .map(...) builds a new array by converting each element of the old array.

    6. Follow POST and DELETE

    For POST, express.json() parses the JSON into req.body. Validation checks that it is an object and that any supplied fields have the right types. Strings stay strings; price and quantity must be nonnegative numbers, and quantity must be a whole number. Omitted fields are allowed, matching the lecture's defaults.

    productService.add selects the six known product fields and inserts them. Mongo creates _id; the caller cannot choose it by supplying id or _id. The controller responds with 201 and { "id": "..." }. Use that id in GET.

    For DELETE, Mongo's deleteOne returns a result with deletedCount. The service turns that into a boolean. The controller sends 204 if one product was deleted, or 404 if none was. Deleting twice should produce 204 then 404.

    Middleware order is important: parse JSON, route the request, handle unknown routes, then handle errors. Error middleware has four parameters, with err first; Express uses that signature to recognize it. Express 5 forwards rejected Promises from async controllers to the error handler.

    7. Run it yourself

    From this repository folder:

    bash
    npm ci
    npm test
    npm run typecheck
    npm run build

    Tests do not need your normal MongoDB server. To use Postman or curl, start your normal MongoDB separately at 127.0.0.1:27017. In Compass, connect to it, create database week4 and collection products, and import sampleData/products.json. Then:

    bash
    npm run dev

    Keep that terminal open. In another terminal:

    bash
    curl -i http://localhost:6790/products
    curl -i -X POST http://localhost:6790/products \
      -H 'Content-Type: application/json' \
      -d '{"modelName":"Practice coffee maker","price":25,"quantity":3}'

    Copy the new id from POST. Replace YOUR_ID below with it:

    bash
    curl -i http://localhost:6790/products/YOUR_ID
    curl -i -X DELETE http://localhost:6790/products/YOUR_ID
    curl -i http://localhost:6790/products/YOUR_ID

    Use a product you created for practice, because DELETE removes it. Expect 200, then 204 with an empty body, then 404. Stop the dev server with Control-C. npm run build followed by npm start runs compiled JavaScript instead of the development watcher. Optional MONGO_URI, DB_NAME, and PORT overrides let you point this app at a separate practice database.

    If you get connection refused, MongoDB is not reachable at the chosen address. If the port is in use, stop the other server or choose another PORT. If GET returns [], check the database/collection and your import. A 400 id error means you did not replace YOUR_ID with a real 24-character id.

    8. What the tests teach

    Vitest runs the tests. Supertest sends HTTP requests to our Express app. A MongoMemoryServer launches a real temporary MongoDB on a separate port. The tests call db.init(mongo.getUri(), 'week4-tests') so they use that database.

    beforeAll starts/connects once. beforeEach empties only that test database's products and inserts the same two coffee makers. afterEach restores the one service mock used for the error test. afterAll disconnects and stops MongoDB. Never call _clearCollection on your normal database just to run these tests.

    Each test follows arrange, act, assert. Setup arranges two known products; a request acts; expect checks the result. GET's setup inserts directly through the database layer so a broken POST does not cause a misleading GET failure.

    Use toBe for values like a status number, toEqual for an object's contents, toHaveLength for array size, and toMatch for an id's shape. Two separate objects can have equal contents while failing toBe, which checks identity.

    The suite checks list shape, creation/readback, successful and absent GET, successful and absent DELETE, empty collections, defaults, invalid input, unknown paths, and unexpected service errors. It runs the source tests once; compiled copies under dist/ are excluded.

    Supertest imports app.ts, so it does not execute index.ts. Passing tests alone do not prove the real server starts. That is why checking npm start and actual HTTP requests is a separate step. GitHub's small classroom check also does not substitute for these route checks or the instructor's review.

    9. Practice and check yourself
    • 1. Trace GET by id through the five layers without running it. At which point

    does the id become an ObjectId? At which point does it become a string again?

    • 2. Create a product with price zero. Explain why ?? preserves that value.
    • 3. Write a test that creates and deletes a product, then confirms GET returns 404.
    • 4. Think through adding PATCH without coding it yet: which layer chooses the

    response status, which layer coordinates the update, and which talks to Mongo?

    • 5. Change a test's expected status to the wrong number, run that test, and read

    the failure. Restore the expectation afterward.

    Answers: ObjectId conversion happens in the database query; document-to-product conversion makes the response id a string. ?? only replaces null/undefined, not zero. The controller chooses HTTP status, the service coordinates the update, and the database layer executes it. A good deletion test checks both the delete response and a later read, rather than trusting the first response alone.

    Suggested first session: spend ten minutes on requests/statuses, fifteen tracing GET through the files, fifteen making POST/GET/DELETE requests, and ten reading and changing one test. If you can explain why a missing product becomes null in the service but 404 in the controller, you understand the central idea of today.

    Code folders and reading order

    FolderRead in this order
    week1hello / Week 1 NYTserver.js → corresponding request in Postman/curl.
    TypeScript basicsREADME.md → src/index.js → src/index.ts → expected demo output.
    Week 2types.ts → books-service.ts → books-router.ts → app.ts → index.ts → tests.
    HW1types/data → auth/validation → routers → app/index → auth/menu/orders/errors tests.
    Week 3db-explore.ts → db.ts → validate-id.ts/product-router.ts → app/index → tests.
    Week 4db/db.ts → models/product.ts → services → controllers → routes/middleware → app/index → tests.

    Week 3 · HTML source · Week 4 · HTML source

    Source trail · Current course pages, local files, and retained guides. Original guide prose is preserved.

    ↑