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

    來源依據 · 目前課程頁面、本機檔案與保留的指南。原始指南文字完整保留。

    ↑