018 / SI 679 · 2026 秋季

后端开发
学习指南。

从第一条 Express 路由,到经过测试的 MongoDB API。阅读完整课程、追踪请求流程,再阖上笔记,试着自己说明。这是一份可以分享给朋友的学习伙伴。

第 1–4 周 + TypeScript + HW142 道回想题6 份完整原始指南
一页完整呈现,依自己的步调学习。搜索概念 /

没有符合的主题。请缩短关键字,或清除搜索。

01

第 1 周 · HTTP、Node、Express、表单与 JSON

请求如何变成回应。

先从双方的对话理解

一个 用户端 提出需求。 服务器 运行程序并送回答案。浏览器、Postman、curl 和 Supertest 测试都能充当用户端。 API 规范允许哪些请求,以及回应代表什么意思。

Node 让 JavaScript 在浏览器以外的环境运行。Express 是一个函数库,协助 Node 程序接收 HTTP 请求、比对路由、解析数据并送出回应。安装 Express 不会自动启动服务器;程序必须创建应用程序,并调用 listen().

用户端→HTTP 请求→Express 处理函数→HTTP 回应
HTTP 请求
POST /tracks HTTP/1.1
Host: localhost:3000
Content-Type: application/json

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

其中的 方法 是 POST, 路径 是 /tracks;标头描述请求,主体则带有提交的曲目数据。回应也有标头、状态码,通常还有主体。URL 包含通信协定、主机、选用的连接端口、路径,以及选用的查找字符串。 localhost 指的是你自己的电脑;连接端口用来指定正在监听的程序。

部分范例意义
方法GET, POST读取或提交数据;方法加上路径决定要使用的路由。
路径/tracks用户端要求访问的端点。
查找字符串?author=Tolkien&year=1937URL 中具名的输入值,常用于筛选。各组键值之间以此符号分隔: &.
标头Content-Type: application/json中继数据;描述提交主体的格式。
主体{"title":"Dune"}提交的内容;常见于 POST 与 PATCH。
回应状态码201 Created表示发生了什么事,与画面上看得到的主体内容是两回事。

知道输入数据放在哪里

来源请求范例读取位置
路径参数/play/artist/Radiohead/song/Creepreq.params.artist 与 req.params.song
查找参数/catalog?itemid=222req.query.itemid
表单主体表单使用 name="zipcode"req.body.zipcode,并先解析 URL 编码数据
JSON 主体POST /tracks 搭配 JSONreq.body.artist,并先解析 JSON
标头Authorization: Bearer SI679req.headers.authorization

在以下路径中的冒号: /person/:id 用来定义预留位置。用户端会使用实际路径,例如 /person/42;不会真的送出 :id。URL 编码会将空白表示为 %20,Express 会将它解码。路由参数是文字;查找值也必须先检查,才能当成单一字符串或数字使用。

在 HTML 中,表单的 action 决定 URL, method 决定使用 POST 或 GET,而每个输入字段的 name 决定主体中的键名。输入字段的 id 用来链接标签,并不决定提交时的键名。邮递区号应保留为字符串,这样 "01234" 才能保留开头的零。

JavaScript · 改编的教学范例
app.use(express.urlencoded({ extended: true })); // HTML form bodies
app.use(express.json());                        // JSON bodies

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

JSON 是具有明确语法的文字格式。 它支持字符串、数字、布尔值、null、数组与对象。键名与字符串值使用双引号。注解、结尾多余的逗号,以及 undefined 都不是合法 JSON。解析后的 JavaScript 对象已经不是 JSON 字符串。 JSON.parse() 将文字转成值; JSON.stringify() 将值转成文字。

由左至右读懂路由

JavaScript · 简化自第 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 用来注册路由;相符的请求到达时,就会运行它的回呼函数。 req 保存请求,而 res 用来创建回应。 res.send() 可以送出文字或 HTML, res.json() 将值串行化成 JSON,而 res.status(400) 先设置状态码再送出回应。 res.sendStatus(404) 设置状态码,并送出该状态的文字名称。 status() 单独使用不会结束回应。

解构赋值,例如 const { itemid } = req.query,会将对象属性取出并指定给区域变量。范本字面值使用反引号与 ${...} 来插入值。使用输入之前必须先验证。在错误回应之后的 return 会停止回呼函数,避免送出第二次回应。

一张表掌握第 1 周所有活动

其中的 week1hello 的跟做范例监听于 1679。第 1 周的 Now You Try 监听于 3000。概念相似不代表路径可以互换。

week1hello 的跟做范例理解重点
GET /, GET /about简单的文字回应与路由注册。
GET /contact → POST /submit显示名字与姓氏表单,再从主体解构取出 fname 和 lname。
GET /people?letter=a使用数组,找出第一个以指定字母开头的名字,忽略大小写。缺少或空白的 letter 需要防护;这个起始范例示范概念,没有完整验证。
GET /league/:leaguename/team/:teamname在同一路由中使用两个具名路径参数。
GET /person/:id转换并检查数值输入;以 400 拒绝无法使用的输入。
POST /postData从已解析的 JSON 读取 firstName 与 lastName。
第 1 周 Now You Try 路由输入与结果
GET /catalog?itemid=222验证查找值是非空白的数字文字;回传描述所要求商品的句子,或回传 400。
GET /play/artist/:artist/song/:song将解码后的两个路径值插入描述正在听音乐的句子。
GET /getform回传地址、城市与邮递区号表单,提交目标为 POST /submitform。
POST /submitform使用表单字段描述寄送目的地。
GET /browse/:category?keywords=coffee结合一个路径参数与一个查找输入。
GET /hithere?name=Ada使用查找值回传问候语。
POST /tracks要求 artist 与 title 都是非空白字符串;回传 status、trackAdded 与 ISO 时间戳记。本机成功状态为 200。
GET /products/:department/:category?keywords=lcd,smart,sony使用 split(",") 再接 join(" AND "),产生 lcd AND smart AND sony。
预测:GET /catalog 没有 itemid 时会如何?为什么?

400。值不是字符串,因此防护条件会在类型转换或成功回应之前停止处理函数。

环境设置与手动用户端练习

Shell · 新练习项目,现有保存库不需要这些步骤
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 创建 package.json,描述项目与脚本。 npm install 将相依套件加入 node_modules。 "type":"module" 选用 ES 模块的 import 语法。start 脚本使用 Node 的监看模式,保存代码就会重新启动进程;内存中的数组也会在重启时消失。

在 Postman 创建 SI 679 集合与各周文件夹,设置方法与完整 URL,再送出 GET /。测试 POST 时,选择 Body → raw → JSON,输入数据,并检查状态码与主体。要先添加路由,才能期待请求成功。讲义使用连接端口 3000,你保存的 week1hello 则使用 1679。

测试 /people 时,试试 letter=c、f、3,再试不带 letter 或错写为 ltr。这能看出「找不到相符的人」和「无法安全地取用输入的索引」之间的差异。笔记中的 /people/33 范例与其 /person/:id 路由不一致;测试实际注册的路由时请用 /person/33。第 1 周商品路由在本机使用 department/category 作为预留位置名称;无论名称如何,公开路径范例都是 electronics/tvs 与 household/kitchen。

来源依据 · 第 1 周课程页面、两个本机 Express 活动,以及第 1 周读书计划。

02

TypeScript 基础 · 图书目录练习

类型描述你的假设。

TypeScript 改变了什么

TypeScript 在 JavaScript 上加入编译时期检查。它能在程序运行前协助找出字段拼字错误、参数类型错误,或可能缺少的值。编译器会产生 JavaScript,交由 Node 运行。类型注记在运行时期会消失;它们不会验证用户端传来的请求。

练习从能正常运作的 src/index.js 图书目录开始,将它转成 src/index.ts ,并保留原本输出。六个编号任务依序是界面、具类型的数组、枚举、函数签章、联集类型别名与泛型。六项都要理解。

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 描述对象的字段。 Book[] 是书籍数组。枚举会替允许的状态值命名。函数签章描述两个输入,以及可能是 Book 或 undefined 的结果。 const 禁止重新指定变量,但仍然可以向其数组 push,或修改对象字段。

使用联集类型前先缩小范围

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 是联集类型:值可能是其中一种。 typeof 检查会将类型缩小到对应的分支。 ?. 是可选串连:若值是 null 或 undefined,就停止访问并产生 undefined。它能避免程序崩溃,却不保证值一定存在。

找不到结果的 .find() 会回传 undefined。Mongo 的 findOne() 找不到结果时会回传 null。这里两者都代表不存在,但它们是不同的 JavaScript 值,声明类型应与实际行为一致。

用泛型保留类型

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 是类型参数,每次调用时指定或由编译器推断。输入数组的元素类型会成为输出类型。如果使用 any,TypeScript 就无法继续保护输入与输出之间的类型关系。空数组则说明了为什么结果可能是 undefined。

类型/语法可以理解为用途
void没有可使用的回传值第 2 周服务中的 addBook。
booleantrue 或 falsecheckoutBook 与 returnBook 回报是否成功。
unknown使用前必须先检查的值检查 HW1 中不可信任的配料项目。
any禁用此值的类型检查宽松的输入类型仍可在运行时期检查;能使用精确类型时,应优先使用。
field?: string字段可能不存在订单的选填客制内容或更新字段。
Partial<Product>Product 的每个字段都变成选填第 3 周更新与第 4 周提供的字段。
Omit<Book, "id">Book 除了 id 以外的所有字段描述可编辑的书籍数据。
Partial<Omit<Book, "id">>不包含识别字段的选填变更第 2 周 updateBook 的签章。
Promise<Product | null>未来取得的商品,或没有结果第 4 周异步 get 服务。

每周都用得到的 JavaScript 工具

操作回传内容/运行行为范例意义
find(predicate)第一个相符项目,或 undefined依 id 找到一项菜单品项。
filter(predicate)包含所有相符项目的新数组保留某位作者的书;保留其他 id 的书,就能移除指定书籍。
map(callback)新数组,每个项目对应一个转换结果文档 → Product,或 Product → 制造商名称。
forEach(callback)对每个项目运行操作;回传 undefined收集所有验证问题。
some(predicate)布尔值:至少有一个相符项目某个尺寸选项是否存在?
every(predicate)布尔值:所有元素都通过筛选后的所有项目是否都属于指定类别?
includes(value)布尔值:是否包含指定项目是否提供某种饼皮?
Object.assign(book, changes)将字段拷贝到现有对象第 2 周的部分更新会直接修改找到的书籍对象。
{ ...fields, id }将可枚举字段拷贝到新对象;后面的键值覆盖前面的用插入后的 id 取代候选 id。
const { id, ...changes } = body将 id 与其余属性分开第 2 周 PATCH 主体。
value ?? fallback只有 null 或 undefined 才使用备用值保留价格 0,与依真假值判断的预设方式不同。

像这样的箭头函数: book => book.author === filter 是传给数组方法的回呼函数。 === 比较时不会自动转换类型:数字 1937 与字符串 "1937" 不同。 async 一定回传 Promise; await 会暂停该异步函数,直到 Promise 成功或失败。它不会让整个 Node 服务器停止运作。

这些项目使用 ES 模块/NodeNext 设置,本机 TypeScript 导入路径通常以 .js结尾,因为 Node 加载的是编译后的 JavaScript。 import type 导入的声明会在运行时期消失;一般 import 则会导入可运行的代码。HW1 使用课程提供的不含扩展名导入设置,请遵循保存库现有设置。

说明图书目录的行为

addBook 将 Book 加入数组。 findBookByTitle 找到一本书,或回传 undefined。 checkoutBook 只有书存在且状态为 available 时才成功;第二次借阅会回传 false。 returnBook 将已存在的书恢复为 available。筛选接受作者字符串或出版年份数字。泛型函数安全地回传第一个项目。

示范数据有 The Hobbit、Dune、Foundation 与 Children of Time。Foundation 一开始是 lost。Dune 第一次借阅为 true,第二次为 false;归还后,状态为 available。转成 TypeScript 不应改变这些运行结果,或 README 中的预期输出。

为什么 TypeScript 的 Product 类型注记无法拒绝 Postman 传来的 {"price":"cheap"}?

类型注记在运行的程序中会被移除。收到的数据必须透过 typeof、Number.isFinite 与 Array.isArray 等运行时期检查来验证。

来源依据 · TypeScript README、src/index.ts、其读书计划,以及第 2–4 周与 HW1 的类型使用方式。

03

第 2 周 · 路由器、中间件、服务与第一批测试

运行顺序也是行为的一部分。

整理路由,同时保留 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.

Router 将相关的处理函数集中管理。应用程序提供挂载前缀,路由器提供相对路径。挂载于 /books 并定义 /:id,就会得到 /books/:id。若在路由器内再次定义 /books,前缀就会重复。

第 2 周将程序分为 types.ts (Book 的数据结构)、 books-service.ts (私有的内存内目录与操作)、 books-router.ts (HTTP 处理函数)、 app.ts (组装应用程序),以及 index.ts (于 6790 监听)。服务导出 addBook、getAllBooks、getBook、updateBook、removeBook 与 _resetCatalog。HTTP 代码向服务取得数据,而不是自行管理数组。

中间件要嘛继续,要嘛回应

JavaScript 风格概略 · 第 2 周顺序
const logger = (req, res, next) => {
  console.log(req.method, req.path);
  next();
};

booksRouter.use(express.json());
booksRouter.use(logger);
booksRouter.post('/', checkAuth, validateBookParams, handler);
解析 JSON→记录请求→验证身分→验证数据→创建数据

一般中间件接受 (req, res, next)。调用 next() 会把控制权交给下一个适用的处理函数。送出回应会结束请求。两者都不做,请求就会一直等待。 next(err) 会将控制权交给错误处理中间件。路由器层级的中间件适用于符合该路由器的流量;单一路由的中间件只适用于该路由。

第 2 周的记录器会记录方法、路径与主机名称。 checkAuth 将 Authorization 以空白分隔后的第二个值与 SI679 比较,失败时回传 403 。这是刻意简化的课堂检查,比 HW1 要求的精确 Bearer 解析宽松。 validateBookParams 要求 title 与 author 都是非空白字符串,否则回传 400。有效 token 加上错误字段会进入数据验证;身分验证失败加上错误字段则会停在身分验证。

第 2 周的确切 API 契约

路由访问保护与输入本机程序的行为
POST /books身分验证加上 title/author 验证;主体包含 title、author、year201,回传新书籍。id 使用 Date.now();预设状态为 available。
GET /books公开;可选用 title、author、year 查找参数200,回传数组;提供的筛选条件皆采精确比对,并同时套用。Year 由 URL 文字转成数字。
GET /books/:id公开;将路径 id 转成数字200,回传书籍;不存在时为 404。
PATCH /books需身分验证;JSON 主体包含 id 与至少一个要修改的字段id 无法使用或没有变更时为 400,不存在时为 404,更新成功为 200。其他字段保留。
DELETE /books需身分验证;JSON 主体包含 id移除后回传 200;此实作在没有相符书籍时也回传 200。
GET /books/badroute公开的教学路由抛出错误,用来练习最后的 500 处理函数。注册顺序在 /:id 之前。

请注意,这里的 PATCH 与 DELETE 使用 /books 与主体中的 id。后面的周次使用 /products/:id。要学的是当下正在研读的实际端点。 Date.now() 是课堂用的 id 产生方式;同一毫秒内的两次调用可能撞号,因此无法提供一般性的唯一性保证。

目录使用 let ,因为重设与移除会替换整个数组。移除时使用 filter 来保留所有其他 id。更新时找到书籍,再使用 Object.assign 套用提供的属性。编译时期的 Omit 本身不会从不可信任的运行时期对象移除禁止的字段。

分打开动程序;错误处理放最后

简化的教学概略
// 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 透过四个参数辨识错误处理中间件: (err, req, res, next)。即使未使用,也要保留第四个参数。将它注册在路由之后,Express 向后寻找时,错误才能到达它。404 处理函数是一般的末端中间件,接住没有任何路由处理的请求;错误处理函数则处理沿着链传递的错误。

测试导入 app.ts,因此导入应用程序不会启动固定连接端口的监听器。Supertest 会管理请求所需的暂时监听器,不必运行 npm run dev。这会测试应用程序行为,但不会运行 index.ts,因此仍需另外手动确认启动流程。

第 2 周测试清单

测试涵盖空目录、精确作者筛选、拒绝缺少或错误 token、空白书名验证、201 创建、依 id GET/404、PATCH 的身分与数据验证及未变动字段,以及 DELETE 身分验证与最终空清单。 beforeEach 会调用 _resetCatalog() ,避免前一个测试的书籍影响下一个测试。

使用 res.body 读取解析后的 JSON,并使用 res.text 读取纯文字。状态码与回应内容都要检查;光看到看似合理的书籍,不能证明端点回传了正确状态码。

POST /books 没有 token,而且 title 空白,会发生什么事?

第 2 周回传 403。只要提交文字是合法 JSON,身分验证中间件会在 title/author 验证器运行前回应。

TypeScript 设置、标头与三个 NYT 练习区块

JSONC · 设置示意
// 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"]
}

将 Express 安装为运行时期相依套件;TypeScript、tsx、@types/node 与 @types/express 提供开发工具与类型声明。Vitest、Supertest 与 @types/supertest 是测试相依套件。tsx 能在开发时运行 TypeScript,但不能取代 tsc --noEmit。透过 .gitignore,让产生的 dist 与安装的 node_modules 不进入 Git。

请求起始行包含方法/路径/通信协定,回应起始行包含通信协定/状态码。两者都有标头、空白行,以及在适用时出现的主体。HTTP 标头名称不区分大小写;Node 会将收到的键名转成小写。在 Postman 加入 My-Header-Field: Woot! ,并检查 req.headers['my-header-field']。比较浏览器与 Postman 的标头;不同用户端会提供不同中继数据。

讲义中间阶段的 POST 与测试先使用 200,之后 NYT 明确要求正确的创建状态 201。你完成的第 2 周程序与测试使用 201。讲义与本机错误处理函数为教学显示堆栈追踪;HW1 则送出一般性的 500,并在服务器记录详细信息。

第 2 周练习区块所有要求的任务
NYT #1 · 筛选实作 title、author 与 year 的精确筛选;结合时使用 AND。创建对应的 Postman 请求。
NYT #2 · 路由行为将 POST 改为 201;添加 GET /books/:id,不存在时回传 404;添加需身分验证的 PATCH /books,要求 id 与至少一个变更字段,成功为 200,数据无效为 400。
NYT #3 · 测试POST 缺少或错误的身分验证 → 403;身分验证有效但 title 空白 → 400,且文字包含 non-blank;先创建两位作者的数据,再确认单一作者的精确结果;DELETE 缺少身分验证 → 403,通过身分验证并删除后 → 空目录。

加入后续中间件与测试时,保留先前的筛选功能。依完成后的 API 契约测试路由行为,不要依讲义中间阶段的回应。即使每个测试单独运行都通过,BeforeEach 重设仍不可少。

来源依据 · 第 2 周课程页面、books-router.ts、books-service.ts、app/index、测试与读书计划。

04

作业 1 · 菜单、角色检查、订单验证与错误

把各部分组合起来:SliceDrop。

四个端点;两种角色

HW1 是使用内存保存菜单与订单的 API,连接端口为 3000。它结合第 1 周输入处理与第 2 周路由器、中间件和测试,没有 MongoDB 持久保存。菜单数据由作业提供;保存的订单会取得服务器产生的 id、status 与 timestamp。

端点谁可以调用预期回应
GET /menu公开200;回传全部六个品项,或依 category 精确筛选。
GET /menu/:id公开200,回传一个品项;未知 id 回传 404 与错误。
POST /orders顾客 token201,回传创建的订单;无效订单回传 400 与 errors。
GET /orders员工 token200,回传所有订单,或依 status 精确筛选。

菜单 id 为 hawaiian, meat-lovers, build-your-own, bread-nugz, dipping-sauce,以及 soda. ?category=pizza 会回传三种披萨。未知 category/status 筛选仍是合法的集合查找,只是没有相符结果:200 与 []。未知的单一菜单 id 则为 404。

HTTP · 课堂 token 取自提供的常数
POST /orders
Authorization: Bearer slicedrop-customer-secret
Content-Type: application/json

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

员工清单使用 Bearer slicedrop-staff-secret。这些字符串是作业示范用的凭证,不能作为正式环境的身分验证设计。有效的顾客 token 不会授予员工访问权。

解析凭证与接受凭证是两回事

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];
}
标头值解析结果接着会发生什么事
缺少null401
Bearer slicedrop-customer-secret候选 token 字符串顾客路由接受;员工路由拒绝。
bearer token, Basic tokennull大小写或验证方案不符合此作业要求的精确格式。
Bearer , Bearer one twonull空 token 或多余空白会遭拒绝。
Bearer nope"nope"解析成功;角色中间件仍以 401 拒绝。

^ 与 $ 让正则表达式比对整个值。Bearer 后面紧接一个空白字符。 [^\s]+ 截取一个以上的非空白字符。解析器是一般函数;两个中间件会读取标头,将候选值与各自角色的常数比较,接着回应 401 或调用 next。

应用程序的处理链为 JSON 解析器 → menu/orders 路由器 → JSON 404 备用处理 → 四参数错误处理函数。POST /orders 的路由器依序运行顾客身分验证 → 处理函数 → validateOrder → addOrder。没有 token 且 JSON 为合法的 {} 时,会在字段验证之前得到 401。

先验证结构;收集所有能安全找出的错误

字段/规则合法条件防护的原因
主体非 null、非数组的对象不能把 null、数组或基本类型值当成订单对象。
customerNametrim() 后不是空字符串的字符串拒绝缺少、类型错误或只有空白的名字。
items非空数组走访之前先检查 Array.isArray。
每个项目含有已存在 menuItemId 的对象访问菜单选项之前先做防护。
quantity至少为 1 的整数拒绝缺少、文字、零、负数或小数的数量。
size / crust / sauce若有提供,必须是正确的字符串,且该菜单品项提供此选项选填字段一旦出现,仍必须验证。
toppings若有提供,必须是数组,且每个元素都是有提供的配料字符串字符串不是配料清单;未知或类型错误的元素都算错误。

validateOrder 回传 string[]。它不会送出 HTTP 回应。处理函数将非空的 errors 数组转成 400 {"errors":[...]},若数组为空,就创建订单。让验证器保持为一般函数,可以重复使用,也更容易测试。

使用 errors.push(...) 来累积问题。如果 items 不是数组,就在回报 name/items 问题后返回,避免运行不安全的循环。若查不到菜单品项,先回报仍可检查的 id 与 quantity 问题,再略过该品项的选项检查。在 forEach 回呼函数中 return,只会停止目前项目的回呼,不会从外层验证器返回。

提供的辅助函数使用 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 是否接受,不要断言某个精确时间。

文件导览与作业测试范围

文件/群组研读重点
app.ts / index.ts解析器、挂载点、备用处理与错误;分开的监听器。
middleware/auth.ts精确的 Bearer 解析,以及分开的顾客/员工访问检查。
routers/menu.ts / orders.ts将每个 HTTP 请求转成查找、验证与数据操作。
validation/validate-order.ts累积字段错误;安全地调用可选项目的辅助函数。
data/menu.ts / data/orders.ts提供的菜单查找;进程内的订单状态与测试重设。
types.ts / constants.ts共用的请求/保存数据结构;练习用 token 与连接端口。
auth.test.ts缺少、无效与角色错误的 token;授权成功情况;身分验证早于数据验证。
menu.test.ts六个品项、category 筛选、客制字段与不存在的 id。
orders.test.ts201、唯一 id、pending 状态、可解析时间、清单/筛选/插入顺序。
errors.test.ts404、全部数据验证错误、格式错误的 JSON,以及模拟 addOrder 失败 → 500。

_resetOrders() 在每个订单测试之前运行。让 addOrder 抛出错误的 mock,可以测试错误流程,不需要真的制造应用程序故障。还原 mock 能避免影响之后的测试。来源数据库中的完整作业教学指南包含逐档说明与 18 题测验。

为什么 /menu?category=unicorn 是 200,而 /menu/unicorn 是 404?

前者要求筛选集合,没有相符项目仍是正确结果。后者要求取得特定资源,但该资源不存在。

来源依据 · 本机 HW1 实作、四个测试档、README,以及完整的 TEACHING_GUIDE。

05

第 3 周 · MongoDB、BSON、CRUD、id 与 Express 集成

将状态移到数据库。

选择数据模型;认识术语

MongoDB 是文档导向的 NoSQL 数据库。SQL 数据库将数据列组成数据表,透过键与联结表达关联,适合关系紧密的数据与关联式查找。Mongo 将文档存于集合,允许嵌套对象、数组,以及字段不同的文档。

讲义提出使用 NoSQL 的三个动机: 可扩充性 (将数据分散到多台机器)、 弹性 (改变文档结构),以及 简易性 (对象形式的数据与查找)。这些都是设计取舍,不代表 NoSQL 一定较快,或 SQL 无法扩充。弹性的保存仍需要应用程序契约、验证,并留意旧文档可能具有不同结构。

MongoDB大致对应的关联式概念范例
数据库数据库week3db, week3app, week4
集合数据表products
文档数据列一笔胶带或咖啡机纪录
字段数据栏name, price, quantity

Mongo 保存的是 BSON,这是一种二进位表示方式,支持 ObjectId、日期等额外类型。JSON 本身支持多种值的类型,并非每个值都是字符串。字段名称是字符串,嵌套对象与数组则让一份文档容纳结构化数据。Node 驱动程序负责在 JavaScript 值与 BSON 之间转换。

本机 Community Edition 服务器是 mongod; mongosh 是交互式用户端,Compass 则是图形界面用户端。 mongodb npm 套件是驱动程序,并不是服务器。MongoDB Atlas 是代管服务。Mongo 也支持复写/容错切换与分片;课堂练习只使用一台本机服务器。

连接、选择数据库,再选择集合

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();

连接服务器、选择数据库与选择集合是不同步骤。数据库可能要等到存入数据或创建集合后,才会出现在清单中。临时探索或测试结束后要关闭用户端;持续运作的应用程序则在启动时连接,并持续使用该连接。

探索程序使用 week3db;第 3 周 Express 应用程序使用 week3app。导入其中一个数据库不会填入另一个。 npm run explore 以监看模式运行临时探索档。如果插入功能保持打开,每次保存或重启都可能添加重复数据。先填入一次初始数据,再禁用插入调用,然后探索读取与更新。

CRUD 使用查找对象与更新操作符

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 });
操作理解重点
insertOne / insertMany创建一份或多份文档。结果回报 acknowledged 与 insertedId,或 insertedCount/insertedIds;不是完整读回的文档。
find(query)回传 Cursor,而不是最后的数组。使用 await cursor.hasNext()/next() 或 await cursor.toArray()。
findOne(query)回传一份相符文档或 null;没有相符结果不会抛出例外。
相等查找{name: "Duct Tape"} 比对相等的字段值。
比较查找$lt 小于, $gte 大于或等于;$gt 与 $lte 是另外两个对应的边界。
updateOne / updateMany对第一笔或所有相符数据套用变更。 {$set:{price:4.29}} 保留其他字段,也可以添加字段。
matchedCount / modifiedCount相符纪录可能本来就具有指定值:matchedCount 为 1,modifiedCount 为 0。以相符数量判断数据是否存在。
deleteOne / deleteMany移除第一笔或所有相符数据;检查 deletedCount。 deleteMany({}) 移除该集合中的所有文档。
其他驱动程序选项replaceOne 替换一份文档; {upsert:true} 可以在不存在时插入; {$inc:{quantity:1}} 增加字段数值。这些是讲义延伸内容,并非路由必备功能。
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
}

名称可能重复。名称筛选能示范查找,但对单一资源 API 而言,使用已知 id 更精确。单笔操作最多影响一笔相符数据;多笔操作影响所有相符数据。更新后应再读取确认,不要直接假设回传结果包含修改后的文档。

id 的格式与是否存在是两个问题

TypeScript
const id = '507f1f77bcf86cd799439011'; // illustrative shape
const document = await db.collection('products').findOne({
  _id: new ObjectId(id)
});

标准的 Mongo 自动产生 ObjectId 是 12 字节的值,通常显示为 24 个十六进位字符。URL 传递的是字符串。若 _id 的值类型是 ObjectId,查找就需要 new ObjectId(id);一般字符串的 BSON 类型不同。Mongo 可以保存其他 _id 类型,但这些练习使用它自动产生的 ObjectId。

创建 ObjectId 之前先拒绝格式错误的 id。 badID123 在你完成的中间件中应回传 400。语法合法但找不到相符文档的 id 回传 404。讲义中的 id 只是示意:要成功查找,请先从自己的 POST、清单或 Compass 拷贝实际 id。

用 HTTP 路由包装 Mongo 操作

db.ts 负责连接、断线与商品操作。它接受 URI/数据库参数,因此同一套程序可以使用练习数据库或暂时测试数据库。路由器负责请求字段与 HTTP 状态码。 app.ts 解析 JSON 并挂载 /products。 index.ts 先等待连接成功,才在 6790 开始监听。

第 3 周路由数据库函数本机成功/不存在的结果
GET /productsgetAllProducts → find().toArray()200,回传数组;空集合回传 []。
GET /products/:idgetProduct → findOne(_id)200,回传文档;若为 null,回传 404。
POST /productsaddProduct → insertOne()201,回传 {id}。
PATCH /products/:idupdateProduct → $set,回传 matchedCount有相符数据就回传 200,即使没有实际变更;相符数量为零时回传 404。
DELETE /products/:iddeleteProduct → deletedCount成功删除为 200;没有相符数据为 404。

GET/PATCH/DELETE 的 id 路由先运行 validateId,因此格式错误的 id 会得到 400。第 3 周 Product 字段为 name, price,以及 quantity。其原始文档回应会暴露 _id;第 4 周加入转换,改用公开的 id 字段。 ProductUpdate 让 PATCH 的字段变成选填。POST 创建已知的三字段商品;PATCH 使用提供的变更。类型注记本身不会强制运行主体的运行时期验证。

_clearProducts() 使用 deleteMany({}),是测试辅助函数,不是路由。只有在刻意连到隔离的测试数据库后才能调用。

第 3 周所有 Now You Try 任务

活动必须能写出并验证的内容
NYT #1 · findPricierTest使用 $gte 找出所有 price ≥ 5 的商品。
NYT #1 · updatePriceTest使用 updateOne 与 $set,将 Scotch Tape 的 price 设为 4.29。
NYT #1 · deleteByNameTest使用 deleteOne,依名称删除 Masking Tape。
NYT #1 延伸 · markOnSaleTest使用 updateMany 与 $lt:5,替相符商品加入 onSale:true;在 Compass 检查不同文档的字段差异。
NYT #2 · PATCH 测试断言格式正确但不存在的 id 回传 404;成功更新只改变提供的字段,并保留其他字段。
NYT #2 · DELETE 测试断言成功为 200、不存在的 id 为 404;操作后再确认文档或清单。
NYT #2 延伸 · id 中间件以 400 拒绝格式错误的 id,并测试每个受影响的 GET/PATCH/DELETE 路由。

目前讲义指出 MongoMemoryServer 内容移到第 4 周。本指南在第 7 章统整测试概念,并在这里与练习实验室保留两周的活动。

PATCH 将 price 设为目前相同的值。matchedCount 1/modifiedCount 0 应该变成 404 吗?

不应该。相符商品存在,这是成功但无实际变更的操作。只根据 modifiedCount 回传 404,会将「未变更」与「不存在」混为一谈。

来源依据 · 完整的第 3 周提供笔记;db-explore.ts、db.ts、product-router.ts、validate-id.ts、测试与读书计划。

06

第 4 周 · REST、资源表述与控制器–服务分层

设计公开的 API 契约。

REST 是一种架构风格

REST 代表 Representational State Transfer,由 Roy Fielding 于 2000 年提出。它的影响力来自以一致的用户端/服务器界面配合网络的 HTTP 模型。讲义将它与 SOAP/CORBA 等较复杂的分布式调用方式比较。REST 并不只是「回传 JSON 的端点」。

限制条件理解重点
一致界面以一致方式辨识资源并与其表述交互;适当的 HTTP 方法与命名有助于做到这点。
用户端–服务器将用户端呈现与服务器职责分开,让两者能独立演进。
无状态每个请求提供理解该请求所需的信息;不依赖先前请求留下的对话脉络。
可缓存回应指出是否允许重复使用;缓存决策会影响数据的新鲜度。
分层系统交互可能经过多个层,每层的可见范围与职责有限。讲义使用代码分层来练习关注点分离。
按需提供代码 · 选用在适当情况下,服务器可以发送可运行代码以扩充用户端。

无状态 不 代表「没有数据库」。商品可以持久保存,而每个请求仍独立说明自己的需求。用户端收到的是资源状态的 表述 ,不是直接控制保存的文档。修改用户端对象不会更新 Mongo,必须再送出请求,要求服务器运行更新。

设计实体 → 表述 → 端点

先从实体与其关系开始,再选择用户端能看见的字段,最后用复数资源名词规划路径,以 HTTP 方法表达动作。数据库的保存安排不应决定每个公开路径。

讲义情境中的表述字段与关系
顾客id、firstName、lastName、zipCode。zipCode 是文字,保留开头的零。
商品id, modelName, modelNumber, manufacturer, color, price, quantity。
订单id、customerId、status,以及以商品 id 为键并附有数量的 items。
关系顾客有订单;每笔订单指向一位顾客,并包含各商品的数量。
端点设计操作
/customersGET 取得集合;POST 创建。
/customers/:idGET 取得一笔;PATCH 更新提供的字段;DELETE 删除一笔。
/productsGET 取得集合;POST 创建。
/products/:idGET 取得一笔;PATCH 更新提供的字段;DELETE 删除一笔。
/customers/:id/ordersGET 取得此顾客的订单;POST 为此顾客创建新订单。

这是 设计情境。第 4 周练习实作商品 GET 清单、POST、依 id GET,以及 DELETE 延伸题;没有实作整个顾客/订单 API 或 PATCH。情境规则:提交订单时检查供货情况并扣除库存;将订单存于顶层集合,以便跨顾客查找。角色权限、搜索、付款与完整验证都属于额外设计工作,不是这个练习已完成的功能。

每一层只负责一件事

路由哪个方法/路径使用哪个控制器?
控制器读取 HTTP 输入;选择状态码与回应。
服务协调商品操作与模型转换。
模型描述公开数据结构;转换文档与字段数据。
数据库连接并运行 Mongo 操作。

请求依序经过路由 → 控制器 → 服务 → 数据库;服务在回传途中使用模型函数转换数据。这些是同一个服务器内的文件夹,不是五个网络服务。模型是服务使用的辅助函数,不是另一个请求端点。

由底层往上创建:先做数据库操作,再做模型、服务、控制器、路由器,以及应用程序/启动流程。数据库代码不需要 Express。服务知道数据库与模型,但不知道 req/res。控制器知道 HTTP 与服务,但不直接查找 Mongo。路由只负责将路径与中间件连接到控制器。

第 4 周源代码文件职责
db/db.tsinit、getAllInCollection、getInCollection、addToCollection、deleteFromCollection;PRODUCTS 常数;测试用 disconnect/_clearCollection。
models/product.tsProduct、ProductFields、productFromDocument、productFromFields。
services/product-service.tsgetAll、get、add、remove;回传应用程序格式的数据或不存在的结果。
controllers/product-controllers.tsgetProducts、getProduct、addProduct、deleteProduct;决定 200/201/204/404。
routes/product-routes.ts对应 GET 清单、POST、GET/:id、DELETE/:id;注册验证中间件。
middleware/*id/主体验证与最后的错误处理。
app.ts / index.ts组装解析器/路由器/备用处理/错误处理链;开始监听前先等待数据库 init。

完整追踪 GET /products/:id

摘录/精简自你的第 4 周代码
// 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 验证在 Mongo 操作之前拦下错误格式。数据库回传文档或 null。服务将已存在的文档转成 Product,或回传 null。只有控制器将不存在的结果转成 404,因为 404 是 HTTP 层的决策。

productFromDocument 明确选取公开字段,并将 document._id.toString() 转成 id。用户端取得字符串 id,不会取得 _id。使用 .map() 转换每份文档,就会产生清单回应。这个转换器是一般函数,不会调用数据库,也不使用 req/res,因此可以独立测试。

追踪请求

选择一个情境,查看请求在哪里结束。这是教学模拟,不会送出网络请求。

    追踪创建与删除流程

    POST 也经过相同分层。express.json 解析 req.body,验证器检查提供的值,服务准备商品字段,Mongo 插入文档并产生 _id,服务再将 insertedId 转成 id 字符串。控制器回传 201,回传 {id}。用该 id 读回商品,确认实际保存状态。

    ProductFields = Partial<Product> 让每个字段都变成选填。 productFromFields 使用以下方式提供空字符串与零的默认值: ??,因此提供的零仍会保留为零。备用 id 使用 Date.now,但插入流程使用 Mongo 实际产生的 id。讲义将 InsertOneResult 传给服务,刻意让少量 Mongo 类型信息进入服务,并非完全封装数据库。

    本机第 4 周服务会选取已知字段,并在插入前移除候选 id;提供的 id/_id 不会决定保存数据的身分。它也会在保存前填入默认值。这是对讲义较简单插入范例的本机改进。主体中间件拒绝数组、非对象,以及提供字段的错误类型;price/quantity 必须是有限的非负数,quantity 还必须是整数。仍允许省略字段。

    DELETE 的数据库回传 deletedCount;服务回报布尔值;控制器在删除成功时回应 204,没有主体 ,不存在时回应 404 。第二次 DELETE 回传 404。讲义允许成功时使用带主体的 200 或无主体的 204;本机第 4 周使用 204,第 3 周则使用 200。

    第 4 周全部任务,包含撰写测试的核心练习

    任务必须理解的内容/证据
    讲义 · GET /products将 products.json 导入 week4/products;创建所有分层;取得两台咖啡机,包含公开的 id 字段。
    讲义 · POST /products在 Postman 送出 raw JSON;取得 201/{id};清单增加为三笔;在 Compass 确认保存的文档。
    NYT #1 · GET /products/:id添加数据库/服务/控制器/路由操作。模型转换器已存在。找到 id → 200 与单一对象;合法但不存在的 id → 404;id 为字符串,不包含 _id。
    NYT #1 延伸 · DELETE将删除流程贯穿各层;选择成功回应;清单缩短,Compass 的数据也一致。
    讲义 · Supertest 设置让 init 接受 URI/数据库名称;暂时 Mongo 只启动一次,每个测试前清空并填入初始数据,结束后断线并停止。
    讲义 · 清单/创建断言恰好两笔初始数据、预期的制造商与顺序、id 格式、不含 _id;POST 后清单有三笔,并包含新的制造商。
    NYT #2 · 撰写测试describe GET /products/:id:200 与指定对象;格式正确但不存在的 id 为 404。从初始清单取得现有 id;使用 toEqual 比较对象。
    NYT #2 延伸 · DELETE 测试删除一笔;断言成功,接着清单长度为一。不存在的 id → 404。操作与验证分成不同请求。
    哪一层应将找不到文档转成 HTTP 404?

    控制器。数据库与服务回传不存在的结果,控制器将它转成 HTTP 契约要求的回应。

    来源依据 · 完整的第 4 周提供笔记(含「Write the tests」)、本机源代码与测试修订,以及第 4 周读书计划。

    07

    第 2–4 周与 HW1 · Vitest、Supertest、数据库隔离与比对器

    让验证证据可以重现。

    三个工具,三项工作

    工具工作它无法证明什么
    Vitest运行 describe/it 测试、生命周期挂钩、expect 比对器与 mock。测试套件通过,只能涵盖其中实际写出的断言。
    Supertest对导出的 Express 应用程序创建 HTTP 请求;检查 status/body/text。导入 app 不会运行 index.ts 的启动流程,也不会测试真正的固定连接端口服务器。
    MongoMemoryServer在独立 URI 启动真正的暂时 mongod,用于数据库测试。它不是 Compass/手动操作所需的持久本机 Mongo 服务器。

    测试专用 URI/数据库将测试数据与一般 week3app/week4 纪录分开。MongoMemoryServer 是真正的数据库进程,不是内存中的 JavaScript mock。暂时保存行为取决于 Mongo/保存设置;隔离来自测试程序连到暂时实例。随机连接端口不是访问控制的安全界线。

    第一次运行可能下载 Mongo 可执行文件;之后使用缓存。运行这些测试套件不必启动一般 mongod 或 npm run dev,但设置或下载错误仍可能让测试无法运行。使用一般持久数据库的测试,可能在多次运行间累积重复插入。暂时实例解决不同运行间的残留;清空/重设挂钩则解决同一次运行中测试之间留下的状态。

    启动一次;每次重设;停止一次

    第 4 周生命周期 · 集成教学概略
    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();
    });
    挂钩时机原因
    beforeAll测试套件/区块开始前运行一次启动数据库与连接成本较高。
    beforeEach每个测试之前已知状态:重设,或清空后填入初始数据。
    afterEach每个测试之后还原 mock 或只为单一测试进行的变更。
    afterAll测试套件/区块结束后运行一次关闭用户端并停止进程。

    第 2 周重设目录数组;HW1 重设订单;第 3 周从空的 products 集合开始;第 4 周清空后插入两台已知咖啡机。这些是不同的测试数据,并非矛盾的测试。第 4 周应直接向数据库填入初始数据,避免有问题的 POST 被误认为有问题的 GET。

    每个测试前都启动 Mongo 成本很高。只填一次初始数据则会让之后测试继承变更。启动一次,在每个测试前以低成本重设,并清理所有自己启动的资源。异步设置与请求都要 await,让断言在工作完成后运行。

    选择能问对问题的比对器

    比对器问题用途/陷阱
    toBe(200)这是相同的值吗?适合基本类型的状态码/字符串/布尔值;对象身分相同与内容相等是两回事。
    toEqual({...})嵌套内容相等吗?适用于对象/数组;数组顺序会影响结果。
    toHaveLength(2)有多少元素/字符?检查数组或字符串长度。
    toContain("Braun")这个值存在吗?检查基本类型的成员或子字符串;新创建的对象使用 toContainEqual。
    .not.toContain("REVOTRA")这个值不存在吗?要搭配正向断言;错误的空清单也能通过只检查不存在的测试。
    toMatch(/^[0-9a-f]{24}$/)字符串符合要求的格式吗?自动产生的 id 应测试格式,不要比对无法预测的精确值。
    toBeUndefined()属性/值是 undefined 吗?第 4 周公开商品不应暴露 _id。
    toBeNull()这是 null 吗?服务/数据库没有相符结果时使用。
    toBeDefined() / toBeTruthy()存在/为真值吗?这些检查比确认精确内容或身分更宽松。
    toThrow()函数会抛出错误吗?传入函数;异步拒绝使用 await expect(promise).rejects.toThrow()。

    expect({a:1}).toBe({a:1}) 会失败,因为它们是分别创建的对象。 toEqual 会成功。对抛出错误的路由送 HTTP 请求,通常会得到应断言的 500 回应;直接调用并遭拒绝的单元测试,才适合使用 rejects.toThrow 。

    第 4 周「撰写测试」任务的示范测试

    Vitest + Supertest · 假设已完成第 7 章设置与两笔初始数据
    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 延伸 · 操作加上独立读回验证
    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);
    });

    准备 → 操作 → 断言。 挂钩准备已知初始数据;请求运行操作;expect 断言结果。PATCH 要比较修改的字段与未修改的字段。POST 要确认 201/id,再读回数据。DELETE 要检查回应与之后的不存在结果。只测状态码,可能漏掉「回报成功但实际没改数据」的服务器。

    第 4 周的基本清单测试检查恰好两笔商品、预期的制造商顺序、不含 REVOTRA、id 为 24 个十六进位字符,以及不含 _id。POST 加入 REVOTRA 后,清单增加为三笔。阅读测试数据,理解这些确切数量与名称的来源。

    读懂失败;理解通过的意义

    先看失败测试名称,以及预期值与实际值。断言失败代表程序至少已运行到能比较行为的阶段。连接/下载/导入错误是设置问题,可能让任何请求都无法运行。逾时可能来自未 await 的操作、等待不存在的 Mongo 服务器,或既不回应也不继续的请求处理函数。

    测试通过不代表 TypeScript 能编译、真正的启动流程会等待 Mongo、导入的范例数据存在,或老师已经评分。检查完整流程时,另外运行类型检查/建置指令,并手动启动应用程序。Classroom 的占位检查不能当成 API 回归测试套件。

    为什么所有 Supertest 测试都通过,npm run dev 却失败?

    测试导入 app,并提供自己的数据库连接,从不运行 index.ts。因此启动设置错误、一般数据库无法连接,或启动时缺少 await,都可能在测试中看不出来。

    来源依据 · 第 2 周/HW1 测试、第 3/4 周完整测试章节,以及两组 Mongo 测试设置。

    08

    练习实验室 · 追踪、预测、撰写与检查

    先试着回答,再揭晓。

    选择一次读书安排

    可用时间建议顺序
    30 分钟10 分钟 HTTP 输入/状态码 → 10 分钟分层/ObjectId → 10 分钟不看笔记回答回想题。
    90 分钟15 分钟第 1–2 章 → 15 分钟中间件/HW1 → 20 分钟 Mongo CRUD → 20 分钟 REST 追踪 → 20 分钟测试/练习。
    多次读书每次读一章。大声解释代码、完成练习,能重现概念后才标记为有把握。

    每题先预测,再展开说明。要运行代码时,使用练习副本或隔离的测试数据库,结束后还原暂时修改。此页只是读书工具,不会调用你的 API 或变更课程数据。

    实验 A · HTTP 与 TypeScript

    1. 标出每个输入来源:POST /tracks?preview=true,带有 JSON title 与 Authorization 标头。

    方法是 POST;路径是 /tracks;req.query.preview 是查找输入;req.body.title 是解析后的主体输入;req.headers.authorization 是标头输入。这个路由没有路径参数。

    2. 写出 /browse/:category 与查找筛选,并解释 /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 是 frozen;keywords 解码后是 organic corn。同一个路由模式可以服务多种类别。

    3. 为什么邮递区号必须是字符串?为什么数值年份筛选需要转换?

    邮递区号是可能以零开头的识别码。模型中的书籍年份是数字,查找却以文字传入;严格相等比较前,必须先将单一字符串查找值转成数字。

    4. 凭记忆重建 TypeScript 练习的六项功能。

    Book 界面;catalog: Book[];BookStatus 枚举;具类型的参数/回传值;BookFilter = string | number 并用 typeof 缩小类型;泛型 getFirstItem<Item> 回传 Item | undefined。将运行示范结果与原本未改动的 JavaScript 输出比较。

    5. getFirstItem([]) 回传什么?const catalog 还能 push 吗?

    空数组回传 undefined。可以 push:const 禁止重新指定 catalog,不禁止修改数组。精确类型注记或非空输入能提供推断所需的元素类型。

    6. 将 "lcd,smart,sony" 转成第 1 周的显示文字。
    JavaScript
    const terms = 'lcd,smart,sony'.split(',').join(' AND ');
    // lcd AND smart AND sony

    实验 B · 中间件与 HW1

    7. 预测第 2 周 POST /books 带有错误身分验证与空白 title 的结果;再预测 HW1 POST /orders 没有身分验证且主体为 {} 的结果。

    第 2 周为 403,HW1 为 401。两者都在字段验证之前停在路由身分验证。若 JSON 文字本身格式错误,则解析器更早拦下。

    8. parseBearerToken("Bearer nope") 会回传 null 吗?

    不会。格式合法,所以回传 "nope"。角色中间件接着拒绝此候选值。解析与授权回答的是不同问题。

    9. 创建至少含三个错误的订单。服务器应如何回应?

    使用空白 customerName、未知 menuItemId 与 quantity 0,并带上顾客 token。预期 400 与 errors 数组,包含能安全找出的问题。没有 token 时预期 401。不要为了找选项错误而读取不存在的菜单品项。

    10. 追踪一个送往未注册路径的请求。

    解析器在适用时运行,路由器没有符合,接着一般的最后备用处理送出 404。不需要发生例外。四参数错误处理函数用于传递过来的错误。

    11. 创建订单、重启 Node,再列出订单。预测结果。

    数组只存在于该进程,因此新启动的应用程序没有保存的订单。使用员工身分验证时,GET 回传 200 与 []。后续练习使用 Mongo 保存,解决应用程序重启后数据保存的问题。

    实验 C · 四个直接 Mongo 任务与 API 检查

    12. 写出 findPricierTest,并预测原始胶带数据中哪些会相符。
    TypeScript
    return await db.collection('products').find({ price: { $gte: 5 } }).toArray();

    原始价格是 5.99、3.99、2.01,只有 Duct Tape 相符。$gte 包含等于边界值 5 的情况。

    13. 写出 Scotch Tape 的价格更新;哪些字段不应改变?
    TypeScript
    return await db.collection('products').updateOne(
      { name: 'Scotch Tape' },
      { $set: { price: 4.29 } }
    );

    name、quantity 与 id 保留。检查 matched/modified 结果,再读取文档确认。

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

    原始测试数据中,Scotch Tape 与 Masking Tape 都低于 5。如果已删除 Masking Tape,就只剩 Scotch Tape 相符。预测必须反映目前数据状态。

    15. 测试 PATCH:如何证明它是部分更新?

    创建胶带,只 PATCH price,再次 GET,断言新价格与未改变的 name/quantity/id。也要断言格式正确但不存在的 id 为 404、格式错误的 id 为 400。判断商品是否存在的是 matchedCount,不是 modifiedCount。

    16. 写出不只检查状态码的 DELETE 测试。

    创建或填入商品、删除它、检查该周指定的成功状态,再 GET 它(404),并列出商品证明它已不存在。删除不存在的 id 得到 404,格式错误的 id 得到 400。

    实验 D · 第 4 周路由与测试活动

    17. 在每个必要分层加入 GET /products/:id。哪一层不需要新的转换器?

    数据库添加依 ObjectId 查找;服务回传 Product | null;控制器读取 params 并选择 200/404;路由对应 GET /:id。既有的 productFromDocument 模型转换器已能处理文档。

    18. 写出「Write the tests」内核练习要求的两个测试。

    有两笔初始商品时,先 GET /products 取得现有 id,再 GET 该 id:200,并以 toEqual 比较预期商品对象。对格式正确但不存在的 id,GET 后断言 404。两个测试都会自动取得 beforeEach 测试数据。完整示范见第 7 章。

    19. 设计顾客/订单路由,不暴露集合保存安排。

    即使订单独立存于自己的集合,GET/POST /customers/:id/orders 仍能表达某位顾客的订单。公开路径反映用户端需求,不必照搬数据库的嵌套安排。

    20. 故意改错测试中的一个预期状态码,运行并读懂失败。之后应还原什么?

    使用练习副本或暂时修改,将预期值改成刻意错误的状态码,运行指定测试,阅读预期/实际输出。还原正确断言,确认测试通过。目的是理解失败,不是放宽断言。

    来源依据 · 练习改编自链接的 NYT 任务与六份现有指南。

    09

    主动回想 · 42 题,可展开答案

    关上笔记。试着解释。

    打开每张卡片之前,先大声回答

    这些题目涵盖完整内核流程。有把握的解释应包含范例,而不只是记住一个词。复习或打印时,可使用上方的「显示所有答案」。

    1. 什么决定 API 路由的唯一性?

    HTTP 方法加上路径模式。GET /orders 与 POST /orders 是不同路由。

    2. req.params、req.query 与 req.body 各对应哪种输入来源?

    依序是路径预留位置、URL 查找键值,以及解析后的提交内容。

    3. HTML 表单输入如何变成对象键名?

    name 属性决定提交的键名;action 与 method 决定端点。

    4. 为什么把主体解析器放最前面?

    后续中间件/处理函数需要解析后的 req.body;格式错误的数据也可能先在解析器失败。

    5. res.status(...).send(...) 后面的 return 有什么作用?

    停止目前处理函数,避免继续运行并尝试送出第二次回应。

    6. 联集类型代表什么?

    值可能是列出的其中一种类型;使用特定类型操作前,先以运行时期检查缩小范围。

    7. 界面与运行时期验证有何差别?

    界面是会被移除的编译时期信息。验证则实际检查运行时期收到的值。

    8. 为什么查找回传 Book | undefined?

    数组中可能没有相符的书籍。

    9. 泛型保留了什么?

    不同调用之间,输入元素类型与输出类型的关系。

    10. Partial 与 Omit 做什么?

    Partial 让字段变成选填;Omit 从编译时期类型排除指定字段。

    11. await 做什么?

    在异步函数内等待 Promise 成功或失败。拒绝结果可能以错误形式继续传递。

    12. 哪些空值会触发 ???零会吗?

    null 与 undefined 会触发。零、false 与空字符串不会。

    13. Router 挂载于 /menu,内部路径为 /:id,合起来是什么?

    公开路径 /menu/:id。

    14. 一般中间件的两种结果?

    调用 next(),或结束回应。next(error) 则将错误往后传。

    15. 如何辨识错误处理中间件?

    使用四参数形式 (err, req, res, next),并注册在相关路由之后。

    16. 为什么中间件顺序会改变状态码?

    前面的中间件可能在后面的验证器或处理函数运行前,就结束请求。

    17. 第 2 周与 HW1 的身分验证失败有何差别?

    第 2 周课堂实作使用 403;HW1 要求 401,包含角色错误的 token。

    18. 合法 bearer 格式与有效凭证有何差别?

    格式让解析器能截取候选值;角色中间件仍必须接受该候选值。

    19. 为什么收集所有订单错误?

    用户端可以一起修正多个字段,结构防护则避免不安全的检查。

    20. 未知的集合筛选应回传什么?

    本练习通常是 200 与 [];与未知单一资源的 404 不同。

    21. 使用内存的服务器重启时会失去什么?

    进程内状态,例如目录/订单数组与计数器。

    22. 集合/文档/字段大致对应什么?

    数据表/数据列/数据栏,但不表示关联式行为完全相同。

    23. JSON 只有字符串吗?

    不是:它包含字符串、数字、布尔值、null、数组与对象。BSON 另外支持更丰富的数据库类型。

    24. find 与 findOne 各回传什么?

    find 回传光标;findOne 回传文档或 null。

    25. $gte、$lt 与 $set 代表什么?

    大于或等于、严格小于,以及只修改或添加提供的字段。

    26. 为什么合法更新的 modifiedCount 可能为零?

    文档已经包含指定值;matchedCount 仍显示它存在。

    27. 为什么要将 URL 字符串转成 ObjectId?

    查找必须使用已保存、自动产生的 _id 所具有的 BSON 类型。

    28. 格式错误的 id 与格式正确但不存在的 id 有何差别?

    格式无法使用时为 400;合法查找找不到资源时为 404。

    29. 为什么 connect/init 要有参数?

    应用程序与测试可以使用不同 URI/数据库,不必修改数据库层代码。

    30. 什么让请求符合无状态原则?

    请求提供解读它所需的信息,不依赖先前请求的对话状态。

    31. 什么是资源表述?

    用户端看得到的资源状态副本;修改副本不会直接修改服务器保存的资源。

    32. 说出 REST 的六个限制条件。

    一致界面、用户端–服务器、无状态、可缓存、分层系统,以及选用的按需提供代码。

    33. 第 4 周应在哪里选择 HTTP 状态码?

    控制器。服务与数据库回传应用程序数据,或不存在的结果。

    34. _id 在哪里变成 id?

    模型层的 productFromDocument 将 ObjectId 转成公开的字符串。

    35. 204 回应有什么特别?

    成功完成,没有回应主体。

    36. 为什么暂时 Mongo 不能取代 beforeEach?

    它在每次运行间都是全新状态,但同一次运行中的测试仍可能共用并修改数据。

    37. 为什么直接透过数据库填入 GET 测试的初始数据?

    让测试准备不必依赖正常运作的 POST 路由。

    38. toBe 与 toEqual 有何差别?

    基本类型值/对象身分相同,与嵌套内容相等的差别。分别创建的商品对象使用 toEqual。

    39. 为什么正向与反向断言要搭配?

    错误的空结果也能符合「不包含 X」;正向断言则证明预期结果存在。

    40. 准备–操作–断言是什么?

    准备已知数据,运行操作,再检查可观察的结果与状态。

    41. 为什么依格式/属性断言自动产生的值?

    id 与时间戳记会变动。应测试要求的格式/可解析性,而非某次运行的精确值。

    42. Supertest 没有涵盖哪些启动行为?

    index.ts 的监听器/数据库初始化,以及一般部署/运行环境设置。

    来源依据 · 集成所有主题的自我检查。

    10

    指令、连接端口、状态码、疑难排解与词汇

    桌边速查。

    在正确的文件夹运行正确的脚本

    在指定项目文件夹打开终端机。只有保存库的 package.json 定义了脚本,它才存在。这些指令供查阅;读书页面不会运行它们。

    项目要知道的脚本一般应用程序/数据库
    Classes/week1hellonpm start连接端口 1679;没有数据库。
    Classes/si-679-f-26-week1-nyt-kelvintigernpm start连接端口 3000;没有数据库。npm test 是占位指令,不是本机测试套件。
    Classes/si-679-f-26-ts-basics-kelvintigertypecheck, build, start, dev, js终端机图书目录;没有 HTTP 服务器,也没有 npm test 脚本。
    Classes/si-679-f-26-week02-nyt-kelvintigertest, test:watch, typecheck, build, dev, start连接端口 6790;内存内的目录。
    Homework/si-679-f-26-hw1-kelvintigertest, test:watch, typecheck, build, dev连接端口 3000;内存内的订单。没有 npm start 脚本。
    Classes/si-679-f-26-week03-nyt-kelvintigertest, test:watch, typecheck, build, explore, dev, start连接端口 6790;应用程序使用 week3app,探索使用 week3db。
    Classes/si-679-f-26-week-04-nyt-kelvintigertest, test:watch, typecheck, build, dev, start连接端口 6790;week4/products;sampleData/products.json。
    指令用途
    npm ci现有工作目录具有有效 lockfile 时,依 lockfile 安装相依套件。
    npm install安装/更新相依套件;讲义起始项目使用此方式。
    npm test在有定义该指令的保存库中,通常以 vitest run 运行一次。
    npm run test:watch源代码改变时重新运行测试。
    npm run typechecktsc --noEmit 检查类型,不产生文件。
    npm run build将 TypeScript 编译到 dist。HW1 使用排除测试的建置设置。
    npm run dev使用 tsx 运行 TypeScript(通常激活监看);不能取代类型检查。
    npm start运行该项目定义的 start:可能是监看的 JS,或已建置的 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.

    预设 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

    预期清单 200、创建 201、读取 200、删除 204 且主体为空,再次读取 404。 -i 显示回应标头/状态码; -X 选择方法; -H 加入标头; -d 提供主体。删除时使用自己新建的练习纪录。

    Shell · 课堂提供的凭证
    # 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'

    HW1 的 POST 与清单请求要在同一个运行中的进程完成,才看得到订单。Shell 中包含 & 的 URL 要加引号,示意 id 则应替换,不要直接照抄。

    不要混淆的状态码与 API 契约

    状态码意义课程范例
    200成功回应GET/清单;第 1 周 tracks;第 3 周 PATCH/DELETE。
    201已创建资源第 2 周 books、HW1 orders、第 3–4 周 products POST。
    204成功,没有主体本机第 4 周 DELETE。
    400无法使用的用户端输入错误 JSON/字段,或格式错误的 id。
    401身分验证凭证不被接受HW1 缺少、无效或角色错误的 token。一般语意与练习的简化角色处理不同。
    403禁止访问一般表示没有权限;第 2 周则用于所有未通过课堂 token 检查的情况。
    404要求的路由/资源不存在格式正确但不存在的 id;未知路由。筛选后集合为空仍是 200。
    500非预期的服务器错误应用程序抛出错误;检查服务器纪录。
    差异较早的范例较后/其他范例
    Id第 1 周菜单形式的文字/第 2 周数值 Date.nowMongo ObjectId 在 URL 中表示为十六进位文字。
    PATCH 路径第 2 周 /books,主体包含 id第 3 周 /products/:id。第 4 周本机练习没有 PATCH 路由。
    DELETE 路径第 2 周 /books,主体中的 id第 3–4 周 /products/:id。
    DELETE 状态码第 2/3 周 200第 4 周本机为 204;笔记允许 200 或 204。
    保存第 2 周/HW1 进程内数组第 3–4 周一般 Mongo 持久保存;测试使用暂时 Mongo。
    回应中的识别字段第 3 周文档显示 _id第 4 周公开 Product 显示 id,省略 _id。
    Product 结构第 3 周 name/price/quantity第 4 周 modelName/modelNumber/manufacturer/color/price/quantity/id。

    由外往内调试

    症状可能需要检查的位置下一步检查
    连接立即遭拒HTTP 监听器或连接端口错误正确的开发服务器有启动吗?检查 1679/3000/6790 与终端机错误。
    等待很久,接着出现 Mongo 连接错误数据库服务器/URI确认指定 URI 能连到 mongod,不能只确认已安装 npm 驱动程序。
    Cannot POST /products/404 HTML方法、挂载位置、已注册路由检查 POST 与 GET,以及路由器的相对路径/前缀。
    body 为 undefined/只有空的默认值Content-Type、主体格式、解析器Postman 使用 raw JSON;express.json 放在路由之前;检查本机验证回应。
    GET 清单为 []选用的数据库/集合/测试数据week3db、week3app 或 week4;是否导入或填入同一位置?
    数据错误之前先出现 401/403身分验证中间件确认课堂凭证与角色完全正确;留意不同周次的状态码。
    YOUR_ID 得到 400id 格式将预留文字替换成实际回传的 24 个十六进位字符 id。
    Headers already sent控制流程检查是否在回应后继续运行,或回应后又调用 next。
    请求卡住中间件/await/连接每个流程都有调用 next 或回应吗?数据库操作完成了吗?
    测试第一次通过,之后却失败共用状态在测试间重设;避免持久数据库残留;还原 mock。
    开发服务器能运行,却有类型错误编译器与运行器tsx 运行与 npm run typecheck 是分开的。
    连接端口已被使用另一个运行中的应用程序停止冲突进程,或使用项目可设置的连接端口。

    依此顺序检查:方法/完整路径 → 状态码 → 标头/主体 → 解析器/中间件 → 处理函数/服务 → 数据库状态 → 回应。遇到 500,读取纪录。测试失败时,查看预期/实际值与测试数据。修正后,重新运行相关测试与类型检查。

    白话词汇表

    术语意义
    端点/路由用户端可访问的操作,由方法与路径模式辨识。
    中间件请求处理链中的函数,可以继续、回应,或传递错误。
    回呼函数传给另一个函数,让它在适当时机运行的函数。
    串行化将值转成可传输的文字或二进位格式,例如 JSON。
    契约用户端与测试所依赖、约定好的输入/输出行为。
    持久保存保存状态能在应用程序进程重启后保留。
    光标用来取得或逐一走访数据库查找结果的对象。
    测试数据/初始数据安排好的已知数据,让测试有明确的预期结果。
    Mock测试中替代原本行为的模拟,例如刻意抛出错误。
    分层/关注点分离一个程序区域负责一项职责,边界清楚。
    表述用户端看得到的资源状态副本。
    运行时期/编译时期程序运行时/检查类型与源代码时。
    模块/导入/导出文件中可重复使用的代码,以及连接到其他文件的声明。
    相依套件/开发相依套件应用程序需要的套件/开发、建置或测试所用工具。
    Lockfile记录解析后的套件版本,以重现相依套件安装。
    监看模式监看的文件改变时,重新运行或启动。
    断言测试中对预期行为的陈述,由比对器检查。
    测试隔离测试结果不依赖其他测试或运行留下的状态。

    来源依据 · 目前七个本机项目的脚本与源代码,以及课程笔记。

    11

    涵盖范围、原始指南与来源数据库

    每个主题都有来源可循。

    查看了哪些内容

    整理于 2026 年 9 月 29 日,数据来自七个本机课程文件夹、其中可用的指南、源代码/测试,以及四个提供的课程页面。本机 Homework 文件夹包含 HW1,未发现本机 HW2/HW3。这是集成第 1–4 周、TypeScript 基础活动与 HW1 的技术读书指南,并非实时 Canvas 作业或截止日期审核。

    两个 Confluence 页面已在浏览器实际呈现后阅读,并保存为详细主题/练习摘录。两份公开 GitHub 笔记已下载,完整主要文字也已保留。四份数据皆可读取。行政公告与历史截止日期保留在来源摘录中,但不当成目前截止日期。来源范例中明显的语法或措辞错误会加以说明,不会直接照抄。

    描述你保存库的确切行为时,以本机代码为依据。只有讲义提出的设计,以及本机改进,都有标示。创建本指南并未修改、提交或重新运行课程作业。先前保存的评分或安装说法,不作为目前证据。

    课程来源涵盖清单

    来源章节涵盖内容研读位置
    第 1 周 · 入门/第一个路由/路由Node/npm/ESM/监看模式、Express app/listen、方法加路径、回应 API打开主题 →
    第 1 周 · POST/表单/表单中间件action/method/name、req.body、URL 编码解析器、文字邮递区号打开主题 →
    第 1 周 · 查找字符串/路由参数/错误req.query/params、解码、数值防护、400/预设 200打开主题 →
    第 1 周 · NYT #1catalog, play, getform/submitform, browse打开主题 →
    第 1 周 · Postman/JSON/NYT #2postData、hithere、tracks/错误/时间戳记、products AND 关键字打开主题 →
    TypeScript 基础 · 六项任务全数涵盖界面、类型注记、枚举、签章、联集/缩小类型、泛型打开主题 →
    第 2 周 · TS 设置/路由/NYT #1NodeNext/build/dev/typecheck、Router/服务、组合精确筛选打开主题 →
    第 2 周 · HTTP 标头/中间件标头中继数据/大小写/自订值、记录器、身分验证、路由顺序打开主题 →
    第 2 周 · 错误/状态码/NYT #2四参数处理函数、badroute、201、依 id GET、PATCH 主体中的 id打开主题 →
    第 2 周 · Supertest/分离 app/状态残留/NYT #3请求链、重设、body/text 断言、身分验证/筛选/删除情况打开主题 →
    HW1 · 完整本机实作与指南菜单/筛选、两种角色、精确解析器、完整订单验证、状态/错误/测试打开主题 →
    第 3 周 · 什么是 Mongo/NoSQL/BSONSQL 取舍、集合/文档/字段、嵌套结构/类型、服务器与驱动程序打开主题 →
    第 3 周 · 数据库/起始项目/连接Compass/mongosh、URI、用户端/数据库/集合/ping/断线、监看模式重复数据打开主题 →
    第 3 周 · 插入/查找/查找/更新/删除insertOne/Many、光标/findOne、ObjectId、比较、$set/结果/选项打开主题 →
    第 3 周 · NYT #1 与延伸较贵商品、Scotch 价格、删除 Masking、多笔 onSale 更新打开主题 →
    第 3 周 · Express/DB/路由器/app/服务器/Postman所有 CRUD 路由、监听前连接、类型结构与状态码打开主题 →
    第 3 周 · 真实数据库测试/暂时 Mongo/比对器跨运行与跨测试的数据干扰、挂钩、比对器/拒绝结果打开主题 →
    第 3 周 · NYT #2 与延伸PATCH 保留字段/404、DELETE 200/404、格式错误 id 的中间件测试打开主题 →
    第 4 周 · REST/原则/情境六个限制条件、表述、实体/字段/端点/规则边界打开主题 →
    第 4 周 · 架构/创建 GET/所有分层各层职责、导入、转换器、map、组装/错误/启动打开主题 →
    第 4 周 · 创建 POST/DB/模型/服务/控制器/路由器/PostmanPartial、??、insertedId、201/id、读回/Compass、疑难排解打开主题 →
    第 4 周 · NYT #1 与 DELETE 延伸GET 单笔/404/公开 id、删除数量/读回、控制器的 HTTP 决策打开主题 →
    第 4 周 · Supertest/暂时 DB/init/挂钩/初始数据/断言app 与启动流程的边界、直接创建数据库测试数据、比对器范例打开主题 →
    第 4 周 · NYT #2/撰写测试/DELETE 延伸测试精确的指定对象/404 测试;成功删除/不存在的删除与清单状态打开主题 →

    你要求涵盖的四个页面

    第 1 周 · 课程介绍、Express 入门 ↗

    实际呈现页面的主题/练习摘录

    第 2 周 · Express 路由 & 中间件、单元测试 ↗

    实际呈现页面的主题/练习摘录

    第 3 周 · MongoDB ↗

    完整主页文字快照

    第 4 周 · REST API 与撰写测试 ↗

    完整主页文字快照

    六份现有指南,都能在这里阅读

    打开即可阅读完整原始说明、运行步骤、练习题与答案。指南已嵌入并排版,不需要 Markdown 阅读器。原始课程文件维持不变;可读的来源快照随指南文件夹一同提供。

    TypeScript 基础 · 完整原始指南

    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.

    第 1 周 · 完整原始指南

    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
    第 2 周 · 完整原始指南

    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 · 完整原始指南

    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.

    第 3 周 · 完整原始指南

    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
    第 4 周 · 完整原始指南

    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.

    代码文件夹与阅读顺序

    文件夹依此顺序阅读
    week1hello/第 1 周 NYTserver.js → Postman/curl 中对应的请求。
    TypeScript 基础README.md → src/index.js → src/index.ts → 预期的示范输出。
    第 2 周types.ts → books-service.ts → books-router.ts → app.ts → index.ts → 测试。
    HW1types/data → auth/validation → routers → app/index → auth/menu/orders/errors 测试。
    第 3 周db-explore.ts → db.ts → validate-id.ts/product-router.ts → app/index → 测试。
    第 4 周db/db.ts → models/product.ts → services → controllers → routes/middleware → app/index → 测试。

    Week 3 · HTML source · Week 4 · HTML source

    来源依据 · 目前课程页面、本机文件与保留的指南。原始指南文字完整保留。

    ↑