在 Postman 创建 SI 679 集合与各周文件夹,设置方法与完整 URL,再送出 GET /。测试 POST 时,选择 Body → raw → JSON,输入数据,并检查状态码与主体。要先添加路由,才能期待请求成功。讲义使用连接端口 3000,你保存的 week1hello 则使用 1679。
提供的辅助函数使用 some 检查可选尺寸,使用 includes 检查可选饼皮、酱料与配料。缺少选项清单代表不提供该选项。即使 NewOrder 有 TypeScript 字段,验证器仍必须检查运行时期的值。
JSON 形式示意 · 注解仅用于说明,不是合法 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.
状态、时间与三种不同失败
NewOrder 描述顾客提交的字段。 Order extends NewOrder 再加上 id、status 与 createdAt。 addOrder 产生像 "1"、"2" 这样的 id,将 status 设为 pending,使用 ISO 时间戳记,并加入内存内数组。未筛选的清单保留插入顺序;status 筛选只保留精确相符项目。重启 Node 会清空订单并重设计数器。
情况
失败位置
回应
格式错误的 JSON 文字
JSON 解析器 → 最后的处理函数
400; {"error":"Request body must be valid JSON"}
合法 JSON,但订单字段无效
一般 validateOrder 函数 → 路由处理函数
400; {"errors":[...]}
缺少 token/角色错误/无效 token
角色中间件
401; {"error":"Unauthorized"}
未知方法/路径
末端中间件
404; {"error":"Not found"}
非预期的应用程序例外
最后的处理函数
500; {"error":"Internal server error"},详细信息记录在服务器上。
解析器错误会被辨识为带有 body 属性的 SyntaxError。非预期例外不会被改成用户端数据验证错误。测试时间戳记时,检查其类型以及 Date.parse 是否接受,不要断言某个精确时间。
这是 设计情境。第 4 周练习实作商品 GET 清单、POST、依 id GET,以及 DELETE 延伸题;没有实作整个顾客/订单 API 或 PATCH。情境规则:提交订单时检查供货情况并扣除库存;将订单存于顶层集合,以便跨顾客查找。角色权限、搜索、付款与完整验证都属于额外设计工作,不是这个练习已完成的功能。
# 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.
预设 Mongo URI 是 mongodb://127.0.0.1:27017。若这是你创建的数据目录,手动启动服务器可以使用 mongod --dbpath ~/data/mdata 。这些是课程设置说明,不代表这台 Mac 目前已安装或正在运行 mongod/mongosh/Compass。
完整的第 4 周手动往返测试
Shell · 只删除你自己创建的练习商品
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
This exercise takes a working JavaScript book catalog and adds TypeScript one idea at a time. Read src/index.ts beside this guide.
Study order
1. Interface:Book describes the required fields and their types. Its
status must be a BookStatus, so an unrelated string cannot be stored.
2. Typed array:Book[] means the catalog may contain only books.
3. Enum:BookStatus gives names to the three allowed status strings and
prevents spelling variations throughout the program.
4. Function signatures: parameter and return types document each function
and let TypeScript check every call.
5. Union type:BookFilter = string | number allows an author or a year.
The typeof check narrows the union before comparison.
6. Generic:getFirstItem<Item> preserves the item type for any array. A
book array produces Book | undefined; a string array would produce string | undefined.
findBookByTitle() and getFirstItem() may return undefined when no item exists. The demo uses optional chaining (?.) so that possibility is handled instead of ignored.
Exercises and checkpoints
Run npm run typecheck. Explain why it prints no output when successful.
Run npm run build && npm start and compare every output line with the
README.
Add a practice book with an invalid status string and read the compiler
error. Remove it afterward.
Call findBooksBy() with a boolean and read the compiler error. Remove the
call afterward.
Call getFirstItem() with [1, 2, 3] and hover over the result in your
editor. TypeScript should infer number | undefined without any.
The central idea is that TypeScript checks assumptions before the program runs. It does not replace the JavaScript program; the build step produces JavaScript that Node executes.
An HTTP request contains a method, a path, headers, and sometimes a body. For example:
text
GET /catalog?itemid=222
GET is the method. /catalog is the path. itemid=222 is a query string. The server matches the method and path to this code:
js
app.get('/catalog', (req, res) => {
// read the request and send a response
});
Checkpoint: explain why typing /submitform into a browser address bar cannot test a POST route. The address bar sends GET, while the route expects POST.
2. Understand the Express setup
At the top of server.js, express() creates the application. Two middleware functions prepare request bodies before routes use them:
express.urlencoded() parses ordinary HTML form submissions. express.json() parses JSON sent by Postman or another program. After parsing, the submitted values are available in req.body.
At the bottom, app.listen(3000) starts the server. Port 3000 is one numbered door on the computer. The full local address is http://localhost:3000.
Exercise:
1. Run npm start.
2. Visit / and /about.
3. Change one response string, save, and observe Node's watch mode restart the
server.
4. Restore the response before continuing.
3. Learn the four places this assignment receives input
Query strings
GET /catalog?itemid=222 places 222 in req.query.itemid. Query strings are good for searches and filters because they do not identify a different route.
The code verifies that itemid is one string, is not blank, and converts to a number. A bad value returns status 400 and the exact required message.
Route parameters
The colons in this route mark changing path segments:
js
app.get('/play/artist/:artist/song/:song', ...)
For /play/artist/Radiohead/song/Creep, Express creates:
URL encoding turns %20 into a space, so The%20Clash reaches the handler as The Clash.
HTML form bodies
GET /getform returns HTML containing a form. The form's action and method tell the browser to send a POST to /submitform. Each input's name becomes a key in req.body:
js
const { address, city, zipcode } = req.body;
The ZIP code stays a string. That preserves a leading zero such as 01234.
JSON bodies
POST /tracks receives JSON. Postman must send Content-Type: application/json, and the server needs express.json() before the route. The route destructures the four fields from req.body and returns a JSON object with the added track and a timestamp.
Checkpoint: for each assignment route, identify whether its inputs come from req.query, req.params, or req.body.
4. Understand validation and status codes
The catalog route returns 400 Bad Request when itemid is not numeric. The track route also returns 400 when artist or title is missing, is not a string, or contains only spaces.
The pattern is:
js
if (inputIsInvalid) {
res.status(400).send('helpful message');
return;
}
The return matters. Once the server sends an error response, the handler must stop. Otherwise it may try to send a second response.
Useful first-week statuses:
200: the request succeeded;
400: the client supplied unusable input;
404: no route matches the requested method and path;
500: application code failed unexpectedly.
Exercise: test /catalog with 222, 3, xyz, a blank value, and no itemid key. Predict the status and body before sending each request.
5. Walk through every assignment route
Route
Input source
Main transformation
GET /catalog
query itemid
validate number and place it in a sentence
GET /play/artist/:artist/song/:song
two route parameters
place both decoded values in a sentence
GET /getform
none
return an address form
POST /submitform
form body
combine address, city, and ZIP code
GET /browse/:category
route parameter + query
combine category and keywords
GET /hithere
query name
create a greeting
POST /tracks
JSON body
validate and return a structured JSON result
GET /products/:department/:category
two route parameters + query
turn comma-separated keywords into AND terms
The product search calls split(',') to make an array and join(' AND ') to make the required display string. For example:
text
lcd,smart,sony -> ['lcd', 'smart', 'sony'] -> lcd AND smart AND sony
The track response uses new Date().toISOString(). The exact time changes on every request, but the ISO format stays predictable.
Week 2 Study Plan: Routers, Middleware, and Supertest
This guide explains the Week 2 class and the finished books assignment. Read the files in this order: types.ts, books-service.ts, books-router.ts, app.ts, index.ts, and finally __tests__/books.test.ts.
What you should be able to do
By the end of the week, you should be able to:
1. use TypeScript types in an Express project;
2. split routes into an Express router;
3. separate data operations from HTTP handlers;
4. write middleware and explain why its registration order matters;
5. use headers for simple token checks;
6. choose appropriate HTTP status codes;
7. split the Express app from the listening server;
8. test requests and responses with Vitest and Supertest.
1. Understand the project layers
The application separates five responsibilities:
text
index.ts -> app.ts -> books-router.ts -> books-service.ts -> catalog array
|
-> types.ts describes the data
types.ts defines what a book looks like.
books-service.ts owns the in-memory catalog and its operations.
books-router.ts translates HTTP requests into service calls.
app.ts assembles the Express application and final error handler.
index.ts opens port 6790 when the app runs as a real server.
This separation makes the code easier to understand and lets tests import the app without starting a fixed network listener.
Checkpoint: explain why a route should call getBook(id) instead of reading the catalog array directly.
2. Review the TypeScript pieces
The Book interface requires an ID, title, author, year, and status. BookStatus limits status to the supplied enum values. The service function signatures show what every operation accepts and returns.
Project-local imports end in .js, even though the source files end in .ts:
ts
import { getAllBooks } from './books-service.js';
TypeScript compiles the files to JavaScript, and Node ultimately loads the .js files. The NodeNext configuration checks imports using those Node ES module rules.
updateBook() accepts Partial<Omit<Book, 'id'>>:
Omit<Book, 'id'> means all book fields except the ID;
Partial<...> makes those remaining fields optional.
That type fits PATCH: a request may change only one field, but it should not replace the book's identity.
3. Learn the service layer
books-service.ts keeps the catalog private and exports focused operations:
Function
Job
addBook
add one book
getAllBooks
return the catalog
getBook
find a book by numeric ID
updateBook
apply selected changes to one book
removeBook
remove the matching ID
_resetCatalog
empty test state before each test
The catalog uses let because removing or resetting books replaces the array. removeBook() uses filter() to keep every book whose ID does not match.
The leading underscore in _resetCatalog() signals that application routes should not call it. It exists so tests remain independent.
Exercise: start with three book IDs on paper and walk through removeBook() for the middle ID. Write the array that filter() returns.
4. Follow middleware in order
Middleware is a function that receives req, res, and next. It either:
Auth runs before validation. A request with no valid token and a bad book body returns 403 because it stops at checkAuth; validation never runs.
checkAuth reads the Authorization request header. The expected value is:
text
Authorization: Bearer SI679
This shared string is only a classroom example. A real application would not hard-code a credential in source code.
validateBookParams requires title and author to be strings containing at least one non-space character. It returns 400 with a helpful message when the submitted book fails that check.
Checkpoint: what happens if middleware neither sends a response nor calls next()? The request stays open and appears to hang.
5. Walk through the API
Request
Auth?
Result
POST /books
yes
validate, create a book, return 201 and JSON
GET /books
no
return all books or exact-match filters
GET /books/:id
no
return one book or 404
PATCH /books
yes
update selected fields, 400 for bad input, 404 if absent
DELETE /books
yes
remove the ID and return 200
GET /books/badroute
no
throw an example error handled as 500
Query filtering
GET /books checks title, author, and year. Every supplied filter must match, so author and year together act like an AND condition. Query values are strings; the code converts year to a number before comparing it with a book's numeric year.
GET by ID
Route parameters are strings. Number(req.params.id) converts the route value before the service compares it with a numeric book ID.
POST status
The assignment changes a successful POST from the default 200 to 201 Created. The response contains the created book, including its generated ID and default available status.
PATCH validation
The body must contain an id plus at least one field to change. The router separates them with:
ts
const { id, ...changes } = req.body;
changes contains everything except id. The service applies those fields to the existing book with Object.assign().
6. Understand the app/server split and error handler
app.ts creates and exports the Express app. index.ts imports it and calls listen(). Supertest imports app.ts, so it can make requests without asking you to start the server first.
The final error handler has four parameters:
ts
(err, req, res, next)
Express uses that four-parameter shape to recognize error middleware. It is registered after the router because Express looks forward through the chain when a handler throws.
Checkpoint: move the error handler above the router in a drawing. Why would an error thrown later fail to reach it?
SliceDrop HW1: A Beginner's Guide to the Finished API
This guide explains the completed SliceDrop menu and orders API from the ground up. It assumes that HTTP, Express, TypeScript, and automated tests are all new. The examples and descriptions match the code and checked-in tests in this repository.
The assignment is small on purpose. It gives one application enough pieces to show the Week 1 and Week 2 ideas working together:
a client sends HTTP requests;
an Express server receives them and runs middleware in order;
routers choose the behavior for a path;
plain functions validate data and look up menu items;
a small in-memory data module stores orders;
the server returns a status code, headers, and a JSON body;
Vitest and Supertest check the observable behavior.
1. The mental model: a client talks to a server through an API
An API is a set of agreed request and response shapes. A client can be a web page, a mobile app, Postman, curl, or a test. The client does not call TypeScript functions such as addOrder directly. It sends an HTTP request.
text
client
│ HTTP request: method, URL, headers, optional JSON body
▼
Express application (`app`)
│ body parser → router → auth middleware → handler → data/validation
▼
HTTP response: status, headers, JSON body
│
▼
client reads the result
For example, a customer placing an order sends:
http
POST /orders HTTP/1.1
Authorization: Bearer slicedrop-customer-secret
Content-Type: application/json
{"customerName":"Ada","items":[{"menuItemId":"soda","quantity":1}]}
The server parses the JSON body, checks the token, validates the order, stores it in memory, and answers with 201 Created and a JSON representation of the new order. The client only needs the API contract: it does not need to know which array or function stored the order.
This project has no database, browser UI, or deployed service. The API is the Express application. The index.ts file is the part that opens a network port when the application is run as a server.
2. HTTP pieces used by this API
Methods and routes
An HTTP method says what kind of operation the client is requesting. A route is the method plus the path pattern. The same path can have different behavior for different methods; GET /orders and POST /orders are separate routes.
Method and path
Who may call it
Behavior
GET /menu
anyone
Return all six menu items, or filter by category.
GET /menu/:id
anyone
Return one menu item by its exact ID, or 404.
POST /orders
customer token
Validate and create an order, or return validation errors.
GET /orders
staff token
Return all stored orders, or filter by status.
The :id in GET /menu/:id is a route parameter. A request for /menu/hawaiian makes req.params.id equal to the string "hawaiian".
Status codes
The status code is a compact description of what happened.
200 OK means the request succeeded. It is used for menu responses and
successful order listings.
201 Created means a new resource was created. A valid POST /orders
returns this status.
400 Bad Request means the client sent something the server cannot process,
such as invalid JSON or an invalid order body.
401 Unauthorized means the request did not present the required valid
token. This assignment uses 401 for a missing, malformed, unknown, or wrong-role token.
404 Not Found means no route handled the path, or a requested menu ID does
not exist.
500 Internal Server Error means application code failed unexpectedly. It
is a server problem, even when it happens while handling a valid request.
The status is part of the API contract. A client can make a useful decision from 201, 400, or 401 even before reading the body.
Headers
Headers are named metadata attached to an HTTP request or response. This API uses two request headers especially:
Authorization: Bearer <token> carries the customer or staff credential.
Content-Type: application/json tells the server that the body is JSON.
express.json() uses the content type and JSON parser to turn a valid JSON request body into req.body. If JSON cannot be parsed, the parser raises an error before an order handler runs.
Body, query string, and route parameters
These are three different places request data can live:
text
GET /menu/hawaiian?category=pizza
└──────┬─────┘ └──────┬──────┘
route parameter `id` query parameter `category`
The body is normally used for data being submitted. The order body contains
customerName and items.
A query string begins after ?. GET /menu?category=pizza puts the value
in req.query.category; GET /orders?status=pending puts the value in req.query.status. Query filters do not create a missing-resource error: an unknown category or status produces 200 with [].
A route parameter is part of the path pattern. In /menu/:id, the value is
available at req.params.id.
Week 1 introduced these ideas with small app.get and app.post handlers. HW1 applies the same ideas after the handlers have been organized into routers.
3. The project map and the app/server split
The important files fit together like this:
text
src/index.ts starts listening on port 3000
src/app.ts builds and exports the Express app
src/routers/menu.ts GET /menu and GET /menu/:id
src/routers/orders.ts POST /orders and GET /orders
src/middleware/auth.ts Bearer parsing and role checks
src/validation/validate-order.ts
order-body validation
src/data/menu.ts supplied menu array and lookup function
src/data/orders.ts in-memory order array and ID generation
src/types.ts shared TypeScript types
src/constants.ts tokens and port
src/__tests__/*.test.ts Vitest/Supertest behavior checks
src/app.ts exports app but does not call listen. src/index.ts imports that app and starts the real server:
ts
import { app } from "./app";
import { PORT } from "./constants";
app.listen(PORT, () => {
console.log(`SliceDrop listening on http://localhost:${PORT}`);
});
This split is important for testing. Supertest can import the app and manage a temporary listener for each test run, so the developer does not have to start the server on its normal fixed port first. Calling listen in app.ts would start that fixed listener whenever the module was imported and could cause port conflicts.
The supplied constants.ts gives the exact development values:
These hard-coded tokens are suitable only for this exercise. The comments in that file point toward a later course topic: real secrets should come from configuration such as environment variables, rather than source code.
4. The middleware chain: order is behavior
Express processes a request by walking through the middleware and route handlers in the order they were registered. Each ordinary middleware has the shape (req, res, next):
inspect or change the request;
send a response and stop, or call next() to continue;
never do neither, or the request can hang.
The finished app.ts registers the shared chain in this order:
The order handlers expect req.body to contain parsed JSON. Therefore express.json() must be registered before both routers. A valid JSON body is available to the order handler; malformed JSON causes the parser to pass an error to the final error handler.
The customer middleware is placed before the handler, so a request with no token gets 401 without being validated. The test named “auth runs before validation” deliberately sends {} with no token and expects 401, not 400. That result is a consequence of registration order, not a special case inside the validator.
GET /orders follows the same pattern with requireStaffToken before the listing handler. A valid customer token is still rejected there because the route requires the staff token.
404 must be after the routers
The 404 handler is a normal fall-through middleware. If it came before the routers, it would answer every request and the routers would never run. After the routers, it means “no earlier route matched.” It returns:
json
{"error":"Not found"}
The error handler must be last and have four parameters
Express identifies error-handling middleware by the exact four-parameter form:
ts
(err, req, res, next)
The finished handler is registered after the 404 handler. The next parameter is not used in this assignment, but it must remain in the function signature so Express recognizes the function as an error handler.
5. Authentication and exact Bearer parsing
The authentication module separates token parsing from Express middleware. That is a useful Week 2 design choice: the parser is a plain function that can be tested without constructing a request, while the middleware handles the HTTP response and next().
What counts as a valid header
parseBearerToken(header) accepts only the exact shape:
text
Bearer <one-or-more-non-whitespace-characters>
The implementation uses the case-sensitive regular expression /^Bearer ([^\s]+)$/.
Header value
Result
missing/undefined
null
Bearer slicedrop-customer-secret
token string
bearer slicedrop-customer-secret
null because Bearer is case-sensitive
Basic slicedrop-customer-secret
null
Bearer
null because the token is empty
Bearer one two
null because the token contains whitespace
Bearer nope
"nope"; parsing succeeds, but authorization rejects it
The parser does not decide whether the token is customer or staff. It only extracts a candidate string or returns null.
What the two middleware functions do
requireCustomerToken reads req.headers.authorization, passes it to the parser, and compares the result to CUSTOMER_TOKEN. If it does not match, it sends:
http
401 Unauthorized
json
{"error":"Unauthorized"}
On a match it calls next(), allowing validation and order creation to run. requireStaffToken has the same structure but compares with STAFF_TOKEN.
The distinction is intentional: possession of a valid customer token does not grant access to the staff-only order list.
6. The menu API
GET /menu
With no query parameter, the menu router returns the supplied menu array directly as JSON. It contains six items:
text
hawaiian, meat-lovers, build-your-own,
bread-nugz, dipping-sauce, soda
With ?category=pizza, the handler uses menu.filter(...) and keeps items whose category exactly equals "pizza". The result contains three pizzas.
The "..." markers above mean additional real fields are present; they are not literal response fields. A category with no matches, such as category=seafood, returns 200 and []. If a query value is not a string (for example, a repeated or unusually shaped query parameter), the handler also returns [] rather than treating it as an exact category.
GET /menu/:id
The handler reads req.params.id and calls findMenuItem(id). That supplied function uses menu.find(...) and returns either a MenuItem or undefined.
For a known ID:
http
GET /menu/hawaiian
the response is 200 and includes the full item, including its three sizes, crust choices, and toppings. For an unknown ID:
http
GET /menu/sushi
the response is:
http
404 Not Found
json
{"error":"Menu item not found"}
Returning [] would be appropriate for an empty collection filter, but a single requested resource that does not exist is represented by 404.
7. Orders: creating and listing data
POST /orders
The request first needs the customer token. Once authorized, the handler calls validateOrder(req.body). An empty error list means the body is valid. A non-empty list is returned as:
http
400 Bad Request
json
{
"errors": [
"customerName must be a non-blank string",
"items must be a non-empty array"
]
}
For valid input, addOrder(req.body) creates and stores the order, and the handler returns 201:
http
POST /orders
Authorization: Bearer slicedrop-customer-secret
Content-Type: application/json
The timestamp in a real response is the current time, so tests check that it is a parseable string instead of comparing one fixed value. IDs are strings and increase from "1"; each call receives a distinct ID while the process is running. Every new order starts with status "pending".
GET /orders
This endpoint requires the staff token:
http
GET /orders
Authorization: Bearer slicedrop-staff-secret
With no filter it returns the current array in insertion order. With ?status=pending, it calls listOrders("pending") and returns only matching orders. An unknown or currently empty status returns 200 and []; it is not an invalid request.
The order data is deliberately in memory. src/data/orders.ts starts with:
ts
let orders: Order[] = [];
let nextId = 1;
addOrder creates an Order, increments nextId, pushes the order, and returns it. listOrders either returns all orders or uses filter for a status. _resetOrders() clears the array and resets IDs for tests. The orders are lost and IDs start over when the Node process restarts; this is not a persistent database.
8. Order validation, including every error
validateOrder(body) is a plain function returning string[]. It does not send an HTTP response. The route handler decides that a non-empty array means 400. Keeping validation separate makes the rules easier to read and test.
The function is defensive because req.body comes from an outside client. A client can send null, a number, an array, or an object with unexpected field types. The implementation checks before reading nested properties.
Request-level checks
1. It checks whether body is a non-null object and not an array. If not, the
values used for validation are treated as undefined.
2. customerName must be a string whose trim() is not empty. This rejects a
missing value, a number, "", and whitespace-only text.
3. items must be a non-empty array. If it is not an array, the function
returns the errors found so far rather than calling .forEach on an unsafe value.
Item-level checks
For every item, the validator checks:
the item is object-like before reading its fields;
menuItemId is a string identifying an existing menu item;
quantity is an integer at least 1;
an optional size is offered by that menu item;
an optional crust is offered by that menu item;
an optional sauce is offered by that menu item;
an optional toppings value is an array, and every topping name is offered.
The validator uses the item index in messages, such as items[0].quantity must be a whole number of at least 1. It does not stop at the first problem. For example, an order with a blank name, an unknown menu ID, and quantity 0 receives all applicable messages in the same errors array. This is more useful to a client than forcing three separate requests.
Option checks happen only when the menu item was found. That avoids trying to read option lists from undefined. It also means an unknown menu item gets the menu-ID error without a misleading size, crust, sauce, or topping error.
The four supplied helper functions at the bottom of the file implement the membership checks:
offersSize returns false when sizes is absent, then uses .some to
compare an option's name.
offersCrust returns false when crusts is absent, then uses .includes.
offersSauce does the same for sauces.
offersTopping returns false when toppings is absent, then uses .some
to compare topping names.
Matching is exact and case-sensitive. "Large" is not the same as "large". The soda item has sizes but no crusts or toppings; a supplied unsupported option therefore fails validation.
9. TypeScript ideas in this code
Interfaces and type aliases
src/types.ts describes the shapes shared across modules.
Category is a union of four allowed strings: "pizza", "appetizer",
"side", and "beverage".
SizeOption and ToppingOption require a name and numeric price.
MenuItem requires id, name, and category; description, price,
sizes, crusts, sauces, and toppings are optional with ?.
OrderStatus is the union "pending" | "in-progress" | "completed".
OrderItem requires menuItemId and quantity, while customization fields
are optional.
NewOrder is the customer-submitted shape.
Order extends NewOrder, so an Order has all of the new-order fields plus
id, status, and createdAt.
An interface describes an object shape; a type alias such as Category or OrderStatus can describe a set of allowed literal values. TypeScript checks these shapes while compiling, but a client can still send bad runtime data. That is why validateOrder still performs real runtime checks.
undefined and narrowing
Optional properties may be absent, so TypeScript represents their values as possibly undefined. findMenuItem returns MenuItem | undefined because a lookup may fail. The code narrows before using the value:
ts
const menuItem = findMenuItem(id);
if (menuItem === undefined) {
res.status(404).json({ error: "Menu item not found" });
return;
}
res.json(menuItem); // here TypeScript knows it is a MenuItem
The validator uses checks such as Array.isArray(items), typeof category === "string", and menuItem === undefined to narrow broad runtime values into safe, more specific ones. The early return statements make those narrowed facts easy to follow.
validateOrder(body: any) intentionally accepts any incoming body so it can inspect malformed values. any turns off compile-time protection for that value, so the function compensates with explicit runtime guards. The topping callback uses unknown, which is safer: a value of type unknown must be checked before treating it as a string.
Arrays and callback methods
Several small array methods express the business rules directly:
ts
menu.find((item) => item.id === id); // one matching item or undefined
menu.filter((item) => item.category === category); // all matches
items.forEach((item, index) => { ... }); // validate every item
options.some((option) => option.name === size); // any option matches
crusts.includes(crust); // exact string membership
The tests also use .map to project order names and .every to verify that a filtered result satisfies a condition. These methods take callback functions: the arrow function receives an element, and the method returns the appropriate result without requiring a manual index loop.
10. File-by-file walkthrough of the completed work
This section follows the changed files from the outside of the application toward the inside.
src/app.ts
1. The imports bring in Express request/response types, the two routers, and
the types needed by the error handler.
2. export const app = express() creates the application object and exports it
for both index.ts and tests.
3. app.use(express.json()) installs the body parser before any route.
4. app.use("/menu", menuRouter) mounts the menu router at the /menu base
path. app.use("/orders", ordersRouter) does the same for orders.
5. The next middleware returns JSON 404 for any request that got through
both routers without a match.
6. The final four-argument middleware distinguishes malformed JSON from other
errors. A SyntaxError with a body property is answered with 400 and { error: "Request body must be valid JSON" }. Any other error is logged with console.error and answered with 500 and { error: "Internal server error" }.
The return statements after responses are control-flow guards. They stop the handler from trying to send a second response.
src/middleware/auth.ts
1. It imports Express types and the two token constants.
2. parseBearerToken handles only extraction. It returns null for a missing
header or a regex mismatch and returns capture group 1 for a valid header.
3. requireCustomerToken calls the parser, sends 401 on any non-matching
token, and calls next() only for the customer token.
4. requireStaffToken repeats that pattern for the staff token.
The parser is deliberately not middleware. A plain function has no response to send and no next() to call, so its job stays focused and direct.
src/routers/menu.ts
1. It imports Express request/response/router types and the supplied menu data
functions.
2. menuRouter = Router() creates a router whose paths are relative to the
mount point in app.ts.
3. menuRouter.get("/", ...) handles the mounted GET /menu. It returns the
whole array when category is absent, returns [] when the query value is not a string, and otherwise filters for an exact category.
4. menuRouter.get("/:id", ...) handles mounted GET /menu/:id. It looks up
the ID, returns a 404 error object when there is no item, and returns the item when there is one.
Because the router is mounted at /menu, its internal / is not just / to the client. The combination is /menu/ (and Express also handles the usual slash variation), while its internal /:id becomes /menu/:id.
src/routers/orders.ts
1. It imports the supplied addOrder and listOrders functions, both auth
middleware functions, and validateOrder.
2. ordersRouter = Router() creates a relative router.
before the handler. The handler collects validation errors, returns 400 with { errors } when needed, and otherwise calls addOrder and returns 201 with the new order.
4. ordersRouter.get("/", requireStaffToken, handler) puts staff auth before
the listing logic. The handler returns all orders without a status query, returns [] for a non-string query value, and passes a string status to listOrders.
The leading slash in each router path is relative to the /orders mount, so the public paths are POST /orders and GET /orders.
src/validation/validate-order.ts
1. It imports the MenuItem type and findMenuItem.
2. It creates an error accumulator, checks whether the body is a non-array
object, and validates customerName.
3. It validates that items is a non-empty array. If not, it returns the
accumulated errors safely.
4. It loops through every item, looks up the menu ID, checks quantity, and
returns the menu-ID and quantity errors as appropriate.
5. When an item and menu item are both safe to inspect, it checks each optional
customization by calling the supplied helper functions.
6. It returns the complete errors array. An empty array is the success signal.
7. The four helpers at the bottom handle option membership and safely treat an
absent option list as “not offered.”
The function deliberately accumulates with errors.push(...) instead of returning immediately from the first bad field. It only returns early when continuing would be unsafe, such as when items is not an array.
src/types.ts
This supplied module is the shared vocabulary. It prevents a menu item from silently becoming an arbitrary object inside the typed parts of the program, documents which fields are optional, and distinguishes a customer-submitted NewOrder from a stored Order with server-generated fields.
src/data/menu.ts
This supplied module defines the six menu items and reusable constants for standard toppings, crusts, and dipping sauces. findMenuItem uses .find and can return undefined. The data is process-local and is not changed by the API routes.
src/data/orders.ts
This supplied module is the exercise's temporary database. addOrder creates the server fields, increments the ID counter, pushes into the array, and returns the object. listOrders preserves insertion order when unfiltered and uses .filter for a status. _resetOrders is test-only and is never called by application routes.
src/constants.ts and src/index.ts
constants.ts holds the two tokens and port. index.ts is the executable entrypoint that starts listening. Keeping this start-up action out of app.ts is what lets Supertest import the app safely.
11. How the tests map to requirements
Vitest supplies describe, it, beforeEach, expect, and test doubles such as vi. Supertest supplies a client-like API:
request(app) gives the exported Express app to Supertest. It does not require the developer to run npm run dev or start port 3000; Supertest manages the temporary listener used for the request. .set adds a header, .send supplies a JSON body, and Supertest parses JSON responses into res.body.
src/__tests__/auth.test.ts
verifies that missing and unknown customer tokens produce 401;
verifies that the customer token allows POST /orders;
verifies that missing, unknown, and customer tokens are rejected by
GET /orders;
verifies that the staff token allows the order list;
verifies that authentication runs before validation by expecting 401 for a
missing token and bad body.
src/__tests__/errors.test.ts
verifies a path no router handles returns 404 with an error field;
verifies invalid orders return 400 with an errors array;
verifies the validator catches unknown menu IDs, quantities below one,
unsupported crusts, and unsupported sizes;
mocks addOrder to throw and verifies an application failure becomes 500,
not a client-facing 400;
sends malformed JSON and verifies the parser error becomes a JSON 400.
The mock test is especially useful because it distinguishes two error sources: the client sent valid JSON and a valid order, but application code still failed.
src/__tests__/menu.test.ts
checks that the unfiltered menu has six items;
checks exact category filtering for pizzas;
checks an unknown category returns an empty successful array;
checks a known item includes customization options;
checks an unknown ID returns 404 with an error field.
src/__tests__/orders.test.ts
checks successful creation returns 201, preserves customer and items,
starts as pending, gives an ID, and creates an ISO-parseable timestamp;
checks two orders receive different IDs;
checks an empty order list before creation;
checks status filtering and the empty result for a status with no matches;
checks that unfiltered results preserve insertion order.
Each order test file calls _resetOrders() in beforeEach. That keeps tests independent even though the application data module is stateful. Tests should not rely on another test having run first.
The timestamp assertion demonstrates a general testing rule: assert a stable property, such as “is a parseable date string,” rather than an unstable exact value, such as the clock time from one particular run.
12. Install, run, test, type-check, and build
Run these commands from the repository directory:
bash
npm install
npm test
npm run typecheck
npm run build
What each command does:
npm install installs the dependencies in package.json and records them
through the lockfile.
npm test runs vitest run, which executes the test suite once.
npm run test:watch runs Vitest interactively and reruns tests as files
change.
npm run typecheck runs tsc --noEmit. It checks types without creating
output files.
npm run build runs tsc -p tsconfig.build.json and emits compiled server
code into dist while excluding tests.
npm run dev runs tsx src/index.ts, starts the server on port 3000, and
prints its local URL. tsx runs TypeScript directly; it does not replace the separate type-check command.
With the development server running, equivalent manual requests include:
The last two requests must occur in the same running process if the listing is expected to show the newly created order. Restarting the process clears the in-memory order array.
The repository README says the short workflow is to install dependencies and make the tests pass, and that the live assignment specification is the primary source when available. The checked-in tests are the concrete, executable examples of the behavior described here.
13. Common mistakes and a debugging routine
Common implementation mistakes
1. Registering express.json() too late. Put it before both routers so
req.body is ready.
2. Putting auth inside validation or after the handler. Auth belongs as the
route-specific middleware argument before the handler. Otherwise a missing token can incorrectly produce 400 or create an order.
3. Using the wrong token for a role. The customer token is valid only for
POST /orders; the staff token is required for GET /orders.
4. Accepting loose Bearer formats. The required parser is exact: capital
Bearer, one literal separating space, and a non-whitespace token with no extra text.
5. Mounting paths twice. Once the app mounts menuRouter at /menu, the
router should define / and /:id, not /menu and /menu/:id.
6. Returning 200 for a missing single item. Unknown /menu/:id values
are 404; unknown collection filters are 200 with [].
7. Stopping validation at the first error. Keep pushing all discovered
problems into the array.
8. Calling .forEach before checking items. Check Array.isArray(items)
first. Likewise, check that a menu lookup succeeded before reading option lists.
9. Confusing malformed JSON with an invalid order. Malformed JSON is a
parser error and gets 400 from the global handler. Valid JSON with bad fields gets 400 with { errors: [...] }. A thrown application exception gets 500.
10. Forgetting to return after sending. A handler that sends an error and
keeps running can attempt a second response and trigger another error.
11. Removing the fourth error-handler parameter. Express uses the four
parameters to recognize error middleware, even when next is unused.
12. Expecting orders to survive a restart. The array is in memory. Restart
means an empty list and IDs beginning again at "1".
13. Testing an exact timestamp. Check its type and parseability, as the
supplied test does.
14. Relying on test order. Reset state in beforeEach; a test should be
able to run by itself.
A practical debugging routine
When a request fails, inspect the problem in this order:
1. Confirm the method and full path. POST /orders and GET /orders are
different, and a router path is relative to its mount point.
2. Check the status code before reading the body. 401 points to auth, 404
to routing or an unknown menu ID, and 400 to parsing or validation.
3. Check the headers. For orders, use the exact token and
Content-Type: application/json.
4. Check the body shape. customerName must be non-blank and items must be
a non-empty array; each item needs a known menu ID and integer quantity.
5. Add a focused test or run one existing test by its description. A failing
test tells you the observable contract rather than an internal guess.
6. If the status is 500, read the server log. The global handler logs the
unexpected error while returning a deliberately generic client message.
7. Run npm run typecheck after fixing behavior. Passing tests alone does not
prove that TypeScript's compile-time checks pass.
When the result seems surprising, trace the chain from the top: JSON parser, mount point, router path, route-specific middleware, handler, data function, then response. That follows the order Express actually uses.
14. Self-check quiz
Try to answer these without looking at the answer key.
1. What is the difference between the exported app and the server started by
index.ts?
2. Which middleware makes a valid JSON body available as req.body?
3. Why must that middleware be registered before the routers?
4. What public path is produced when app.ts mounts menuRouter at /menu
and the router defines get("/:id", ...)?
5. Which token and method are required to create an order?
6. What does parseBearerToken("Bearer nope") return, and where is the token
accepted or rejected afterward?
7. What response should a request with no token and body {} receive for
POST /orders?
8. What is the difference between an unknown category and an unknown menu ID?
9. Why does validateOrder return an array instead of immediately responding?
10. Name three conditions that make quantity invalid.
11. Why does the validator check menuItem === undefined before checking a
size or topping?
12. What does Order extends NewOrder communicate?
13. Why does the error handler need four parameters if it does not call
next()?
14. How does the application distinguish malformed JSON from an application
exception?
15. What happens to orders when the Node process restarts?
16. Why do order tests call _resetOrders() before each test?
17. Which command runs tests once, and which command checks types without
emitting JavaScript?
18. What status should a successful order creation return, and why is that more
specific than 200?
Answer key
1. app.ts constructs and exports the Express app for reuse; index.ts
imports it and calls listen on port 3000.
2. express.json().
3. It must parse the body before handlers that read req.body; its position in
the chain controls whether the routes see parsed data.
4. GET /menu/:id, such as GET /menu/hawaiian.
5. POST /orders requires the exact customer token
slicedrop-customer-secret in Authorization: Bearer ... form.
6. It returns the string "nope"; the customer or staff middleware compares
that candidate with its required constant and rejects it with 401.
7. 401 Unauthorized, because auth runs before validation.
8. An unknown category is an empty successful collection (200, []); an
unknown single menu ID is 404 with an error object.
9. A plain function can be tested and reused independently of Express; the
route handler translates its returned errors into an HTTP response.
10. It is missing, not a number, not an integer, zero, or negative. Fractions
are also invalid.
11. A failed lookup returns undefined, so reading option properties would be
unsafe and could cause a server exception.
12. A stored Order has all NewOrder fields plus server-generated id,
status, and createdAt.
13. Express recognizes error middleware by (err, req, res, next); the fourth
parameter is part of that recognition rule.
14. The handler checks for SyntaxError with a body property and returns
400; all other errors are logged and return generic 500.
15. The in-memory array disappears, so orders are lost and the next ID starts
at "1" in the new process.
16. To clear shared in-memory state and keep tests independent of execution
order.
17. npm test runs the suite once; npm run typecheck runs tsc --noEmit.
18. 201 Created, because the request created a new order resource and the
status communicates that fact to the client.
If the answer to any question was uncertain, trace the matching request through the middleware chain and then read the corresponding test. That is the same method used to understand the rest of this API: identify the request, follow the ordered handlers, and inspect the status and JSON response.
Week 3 Study Plan: MongoDB, Express, and API Tests
Use this guide with the completed code in src/. It starts with the ideas behind the code, then walks through each file and gives you small checks to make sure you can explain what is happening.
What you should be able to do
By the end of this week, you should be able to:
1. explain how a MongoDB collection and document compare with a SQL table and row;
2. connect a TypeScript program to a MongoDB database;
3. create, read, update, and delete documents;
4. place database functions behind Express routes;
5. recognize a MongoDB ObjectId and reject a malformed one;
6. use Vitest, Supertest, and MongoMemoryServer to test an API without changing a real database;
7. use test hooks to give every test a clean starting state.
1. Start with the database vocabulary
MongoDB is a document-oriented NoSQL database. A useful first comparison is:
MongoDB
Rough SQL comparison
Example in this project
database
database
week3app or the test database
collection
table
products
document
row
one Duct Tape product
field
column
name, price, or quantity
A product document looks much like a JavaScript object:
ts
{
name: 'Duct Tape',
price: 5.99,
quantity: 120
}
MongoDB adds an _id field when the document is inserted. The value is an ObjectId. Its printed form is a string of 24 hexadecimal characters, but the database value is an ObjectId, not an ordinary string.
Checkpoint:
Which collection stores the records in this app?
Which part uniquely identifies one product?
Why must new ObjectId(id) be used before searching by _id?
2. Practice Mongo operations directly
Read src/db-explore.ts first. It is a scratch program for learning the database driver before Express is involved.
Mongo queries are objects. { name: 'Masking Tape' } means “documents whose name equals Masking Tape.” { price: { $gte: 5 } } means “documents whose price is greater than or equal to 5.” The $set update operator changes only the fields named inside it.
The four Now You Try functions demonstrate those ideas:
findPricierTest() uses $gte to find products costing at least $5.
updatePriceTest() changes only Scotch Tape's price.
deleteByNameTest() deletes a document using a name query.
markOnSaleTest() uses updateMany() to add onSale: true to every
product costing less than $5.
Exercise:
1. Start local MongoDB.
2. Uncomment await addProducts() in main() and run npm run explore once.
3. Comment it again so repeated saves do not create duplicates.
4. Run each Now You Try function one at a time and print the result.
5. After each operation, print getAllProducts() and predict the output before
looking at it.
3. Separate database work from HTTP work
src/db.ts is the database layer used by the application. It keeps the MongoDB driver details in one place. Express routes do not need to know how a client is created or which collection method performs an update.
The public functions form a small interface:
Function
Input
Output
connect
URI and database name
an open connection
getAllProducts
none
every product document
getProduct
string ID
one document or null
addProduct
a Product
the new ObjectId
updateProduct
ID and partial changes
number of matched documents
deleteProduct
ID
number of deleted documents
ProductUpdate makes each property optional. That matters because a PATCH request can change only price while leaving name and quantity alone.
_clearProducts() is intentionally marked as a test helper. Calling it in the running app would erase the whole products collection.
Checkpoint:
Why does updateProduct() return matchedCount instead of the full driver result?
How does a route use matchedCount === 0 to choose an HTTP status?
Why does connect() accept its URI and database name as arguments?
4. Map CRUD operations to HTTP routes
src/product-router.ts maps the database functions to an HTTP API:
Request
Database action
Successful response
GET /products
read all
200 and a JSON array
GET /products/:id
read one
200 and one JSON document
POST /products
create
201 and { "id": ... }
PATCH /products/:id
update selected fields
200
DELETE /products/:id
delete one
200
A valid-looking ID that is absent from the database produces 404 Not Found. A malformed ID produces 400 Bad Request. These are different failures: the first identifies no document, while the second is not a usable MongoDB ID.
src/validate-id.ts handles malformed IDs before a route calls the database. Without that middleware, new ObjectId('badID123') throws and looks like a server failure. ObjectId.isValid() lets the app report the client's input error accurately.
src/app.ts first registers express.json(), then mounts the router at /products. The parser must run first so a POST or PATCH handler can read req.body.
src/index.ts opens the real database connection before the server listens. Tests import app.ts instead, connect to their own temporary database, and do not run index.ts. This is why the app and server are split into two files.
Exercise:
1. Trace PATCH /products/abc... from app.ts to the response.
2. Write down each function it passes through.
3. Repeat with a malformed ID and notice where the path stops.
5. Understand the test setup
The tests use three tools with different jobs:
Vitest runs test files and provides describe, it, expect, and hooks.
Supertest sends HTTP requests directly to the Express app.
MongoMemoryServer starts a temporary real MongoDB process for the tests.
The hooks establish a predictable lifecycle:
text
beforeAll: start temporary MongoDB and connect once
beforeEach: empty the products collection
test: arrange data, send request, check response and state
afterAll: disconnect and stop temporary MongoDB
Cleaning before every test prevents one test's documents from changing a later test's result. Stopping MongoDB in afterAll prevents the test process from hanging.
The main matchers in this project are:
toBe() for exact primitive values such as a status code;
toEqual() for arrays or objects with the same contents;
toMatch() for the 24-character ID pattern;
toHaveLength() for array size;
toContain() and .not.toContain() for membership.
The PATCH test checks more than the changed price. It also checks that name and quantity stayed the same. That is what proves the code performs a partial update.
The DELETE test verifies both the response and the database state by trying to read the deleted product and by checking the final product list.
6. Suggested study session
First 20 minutes: vocabulary and data flow
Draw the path from an HTTP request to MongoDB and back.
Label the app, router, database layer, collection, and response.
Explain collection, document, field, and ObjectId aloud.
Next 25 minutes: direct Mongo practice
Work through db-explore.ts one function at a time.
Predict what each query matches.
Verify the resulting documents in getAllProducts() or MongoDB Compass.
Next 25 minutes: API code
Read db.ts, then product-router.ts, then app.ts, then index.ts.
For each route, identify its input, database function, success status, and
missing-resource behavior.
Final 20 minutes: tests
Run npm test.
Temporarily change one expected status to the wrong value and read the
failure. Restore it afterward.
Explain why every hook is beforeAll, beforeEach, or afterAll.
Add one extra GET-by-ID success test on your own.
Final self-check
You are ready to move on when you can answer these without opening the code:
1. What is the difference between a MongoDB document and a JavaScript object?
2. Why is a string ID converted to an ObjectId?
3. What do $gte and $set do?
4. Why does PATCH use optional fields?
5. Why do malformed and missing IDs have different status codes?
6. Why does each test need a clean collection?
7. Why does the test suite import app instead of index?
Useful commands:
bash
npm run typecheck
npm run build
npm test
npm run explore
npm run dev
Start with this: a client asks for something, the server does the work, and the server sends an answer. In this project, the thing is a product stored in MongoDB. The client could be Postman, a browser app, or one of our tests.
The lecture and Now You Try instructions are the source for this exercise. We built the lecture's GET and POST routes, GET by id for Now You Try #1, its tests for #2, and both DELETE stretch items.
1. What an API request contains
An API is an agreed way for one program to ask another program to do something. Here the agreement uses HTTP. A request has a method and a path, and sometimes a body. For example:
text
GET /products
GET means read. /products names the collection of products. The server answers with a status code and a JSON body. JSON is text representing objects and arrays. It looks like JavaScript, but keys must have double quotes.
The outer square brackets mean an array. Each object is one product. That id is an illustration; use an id from your own response when making a request.
2. What REST adds
REST gives us principles for keeping the client/server agreement consistent. Use resource names such as /products and let the method describe the action.
Method and path
Meaning
Success code
GET /products
Read the collection
200
GET /products/:id
Read one product
200
POST /products
Create a product
201
DELETE /products/:id
Remove a product
204
:id is a placeholder in our route definition. A real client replaces it with the product's id. GET /products/507f1f77bcf86cd799439011 is a concrete request.
200 means success with an answer. 201 means created. 204 means success with no response body. 400 means the request is malformed. 404 means the resource or route does not exist. 500 means an unexpected server failure.
REST also says requests should be stateless: the request supplies what is needed to handle it. The server does not need to remember which product you asked for last time. This does not mean the database cannot store products between requests.
The client receives a representation of a product: a JSON copy of its current fields. It does not receive direct control of the database document. If it changes its local copy, the stored product does not change.
The lecture's six constraints are a uniform interface, client/server separation, stateless requests, cacheability, a layered system, and optional code on demand. Our exercise concentrates on naming, HTTP methods, representations, and layers. It does not implement every feature of a complete shop API.
3. Why split the code into layers?
We could put a database query inside every route. It works initially, but then database details, HTTP details, and application decisions are mixed together. This week separates those jobs:
The model helps the service turn the document into a product. These are folders inside one server program, not separate servers.
File
Its job
src/index.ts
Connect to MongoDB, then start listening
src/app.ts
Assemble Express and middleware
src/routes/product-routes.ts
Match methods/paths to controllers
src/controllers/product-controllers.ts
Read the request and send the HTTP response
src/services/product-service.ts
Coordinate product operations and conversion
src/models/product.ts
Describe the Product type and convert fields/documents
src/db/db.ts
Connect and run MongoDB operations
src/middleware/validate-id.ts
Check the path id before using it
src/middleware/validate-product.ts
Check supplied product fields
src/middleware/error-handler.ts
Send a response for unexpected errors
src/__tests__/products.test.ts
Check behavior against a temporary database
The route knows about the controller. The controller knows about the service. The service knows about models and the database. The database does not know about Express. That direction matters: changing the HTTP response should not require changing a Mongo query.
4. Follow GET for one product
Open product-routes.ts. This line registers the route:
app.ts mounts this router at /products, so the complete path is /products/:id. validateId runs first. A Mongo ObjectId is represented here by 24 hexadecimal characters (digits and letters a–f). An invalid shape receives 400.
The controller reads req.params.id. params is where Express puts values matched by path placeholders. The controller calls:
The service calls db.getInCollection(db.PRODUCTS, id). The database converts the string to an ObjectId and searches for a document whose _id matches:
ts
findOne({ _id: new ObjectId(id) })
If nothing matches, Mongo returns null. The service returns null too. The controller turns that into 404. The service does not choose 404, because that is an HTTP decision.
If a document exists, productFromDocument creates a new object containing only the product fields. Mongo's _id becomes a string named id. The controller calls res.json(product), which sends that object with status 200.
Read it as: "get takes a string id, does asynchronous work, and eventually gives back a Product or null."
const declares a name that cannot be reassigned.
async means the function returns a Promise: an eventual result.
await waits for that eventual result before continuing this function.
Product | null means either a Product or no matching product.
Product[] means an array of Products.
Promise<void> means asynchronous work with no useful return value.
type Product describes an object's fields for TypeScript. It helps the compiler catch mistakes; it does not check incoming JSON at runtime. Middleware does that. import type brings in a type for the compiler. Normal import brings in code that runs. Imports end in .js because that is the extension after compilation.
Partial<Product> makes every Product field optional. The lecture uses it for incoming fields. fields.price ?? 0 supplies zero when price is missing, while preserving a supplied zero. .map(...) builds a new array by converting each element of the old array.
6. Follow POST and DELETE
For POST, express.json() parses the JSON into req.body. Validation checks that it is an object and that any supplied fields have the right types. Strings stay strings; price and quantity must be nonnegative numbers, and quantity must be a whole number. Omitted fields are allowed, matching the lecture's defaults.
productService.add selects the six known product fields and inserts them. Mongo creates _id; the caller cannot choose it by supplying id or _id. The controller responds with 201 and { "id": "..." }. Use that id in GET.
For DELETE, Mongo's deleteOne returns a result with deletedCount. The service turns that into a boolean. The controller sends 204 if one product was deleted, or 404 if none was. Deleting twice should produce 204 then 404.
Middleware order is important: parse JSON, route the request, handle unknown routes, then handle errors. Error middleware has four parameters, with err first; Express uses that signature to recognize it. Express 5 forwards rejected Promises from async controllers to the error handler.
7. Run it yourself
From this repository folder:
bash
npm ci
npm test
npm run typecheck
npm run build
Tests do not need your normal MongoDB server. To use Postman or curl, start your normal MongoDB separately at 127.0.0.1:27017. In Compass, connect to it, create database week4 and collection products, and import sampleData/products.json. Then:
Use a product you created for practice, because DELETE removes it. Expect 200, then 204 with an empty body, then 404. Stop the dev server with Control-C. npm run build followed by npm start runs compiled JavaScript instead of the development watcher. Optional MONGO_URI, DB_NAME, and PORT overrides let you point this app at a separate practice database.
If you get connection refused, MongoDB is not reachable at the chosen address. If the port is in use, stop the other server or choose another PORT. If GET returns [], check the database/collection and your import. A 400 id error means you did not replace YOUR_ID with a real 24-character id.
8. What the tests teach
Vitest runs the tests. Supertest sends HTTP requests to our Express app. A MongoMemoryServer launches a real temporary MongoDB on a separate port. The tests call db.init(mongo.getUri(), 'week4-tests') so they use that database.
beforeAll starts/connects once. beforeEach empties only that test database's products and inserts the same two coffee makers. afterEach restores the one service mock used for the error test. afterAll disconnects and stops MongoDB. Never call _clearCollection on your normal database just to run these tests.
Each test follows arrange, act, assert. Setup arranges two known products; a request acts; expect checks the result. GET's setup inserts directly through the database layer so a broken POST does not cause a misleading GET failure.
Use toBe for values like a status number, toEqual for an object's contents, toHaveLength for array size, and toMatch for an id's shape. Two separate objects can have equal contents while failing toBe, which checks identity.
The suite checks list shape, creation/readback, successful and absent GET, successful and absent DELETE, empty collections, defaults, invalid input, unknown paths, and unexpected service errors. It runs the source tests once; compiled copies under dist/ are excluded.
Supertest imports app.ts, so it does not execute index.ts. Passing tests alone do not prove the real server starts. That is why checking npm start and actual HTTP requests is a separate step. GitHub's small classroom check also does not substitute for these route checks or the instructor's review.
9. Practice and check yourself
1. Trace GET by id through the five layers without running it. At which point
does the id become an ObjectId? At which point does it become a string again?
2. Create a product with price zero. Explain why ?? preserves that value.
3. Write a test that creates and deletes a product, then confirms GET returns 404.
4. Think through adding PATCH without coding it yet: which layer chooses the
response status, which layer coordinates the update, and which talks to Mongo?
5. Change a test's expected status to the wrong number, run that test, and read
the failure. Restore the expectation afterward.
Answers: ObjectId conversion happens in the database query; document-to-product conversion makes the response id a string. ?? only replaces null/undefined, not zero. The controller chooses HTTP status, the service coordinates the update, and the database layer executes it. A good deletion test checks both the delete response and a later read, rather than trusting the first response alone.
Suggested first session: spend ten minutes on requests/statuses, fifteen tracing GET through the files, fifteen making POST/GET/DELETE requests, and ten reading and changing one test. If you can explain why a missing product becomes null in the service but 404 in the controller, you understand the central idea of today.