第 2 週 · API 設計與接上前端
留言板 API 原型,以及第 2 週複習
把路由、驗證、CORS 收成一個還在記憶體裡的留言板,並看清它為什麼還不能上線。
正在打開這一關…
學習目標
- 完成 GET、POST、DELETE 的記憶體版留言板
- 用一頁 HTML 與 fetch 操作它
- 列出這份原型不能當正式儲存的原因
這一週的零件今天拼起來:REST 路徑、輸入驗證、CORS、環境變數。資料仍放在程式裡的陣列。它是原型,用來確認 API 的形狀,不是客人的留言會在的地方。
用生活例子理解
陣列像櫃檯上的白板。
- 這台 Worker 重啟,白板就擦掉。
- 平台可能同時跑很多份你的程式。客人 A 寫在某一塊白板,客人 B 連到另一塊,看不到 A 的字。
- 白板也沒有權限。任何人知道網址就能刪。
生活比喻:白板適合彩排走位。正式演出要換成會留檔的帳本。那是下一週的資料庫。
所以今天的完成標準是:本機能新增、列表、刪除,而且你能說出「為什麼這還不能佈署給真的使用者」。
動手做
大約 30 分鐘。完整專案在 examples/02-guestbook-memory/。
- 複製該資料夾,或把案例的三個檔案放進新專案。
npm install後npm run dev。- 用瀏覽器打開 Worker 網址的
/。同一來源之下,這頁呼叫/api/messages不必靠 CORS。 - 新增兩則留言,刪掉一則,重新整理。白板還在,是因為開發伺服器還沒關。
- 停掉 Wrangler 再啟動,留言應消失。把這個現象寫進筆記。
- 用
curl送一筆超過 500 字的內容,確認是400。 - 做第 2 週測驗。通過後才打得開資料庫那一週。
案例
const messages = [];
let nextId = 1;
function readMessage(data) {
if (!data || typeof data !== "object") return { error: "請傳送 JSON 物件" };
const author = typeof data.author === "string" ? data.author.trim() : "";
const body = typeof data.body === "string" ? data.body.trim() : "";
if (author.length < 1 || author.length > 20) return { error: "作者請介於 1 到 20 個字" };
if (body.length < 1 || body.length > 500) return { error: "留言請介於 1 到 500 個字" };
return { author, body };
}
async function handleApi(request) {
const url = new URL(request.url);
const one = url.pathname.match(/^\/api\/messages\/(\d+)$/);
if (url.pathname === "/api/messages" && request.method === "GET") {
return Response.json(messages);
}
if (url.pathname === "/api/messages" && request.method === "POST") {
let data;
try {
data = await request.json();
} catch {
return Response.json({ error: "請傳送 JSON" }, { status: 400 });
}
const parsed = readMessage(data);
if (parsed.error) return Response.json({ error: parsed.error }, { status: 400 });
const message = {
id: nextId,
author: parsed.author,
body: parsed.body,
createdAt: new Date().toISOString(),
};
nextId += 1;
messages.push(message);
return Response.json(message, { status: 201 });
}
if (one && request.method === "DELETE") {
const id = Number(one[1]);
const index = messages.findIndex((item) => item.id === id);
if (index === -1) return Response.json({ error: "找不到留言" }, { status: 404 });
messages.splice(index, 1);
return Response.json({ deleted: id });
}
return Response.json({ error: "找不到這個路徑" }, { status: 404 });
}
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname.startsWith("/api/")) return handleApi(request);
return env.ASSETS.fetch(request);
},
};靜態頁面由 Workers 的靜態資源綁定來送。設定:
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "guestbook-memory",
"main": "src/index.js",
"compatibility_date": "2026-10-30",
"assets": {
"directory": "./public",
"binding": "ASSETS",
"run_worker_first": true
}
}run_worker_first 讓每個請求先進入你的程式。API 由你回答,其他路徑再交給 env.ASSETS.fetch,也就是 public/ 裡的檔案。若沒有這行,平台會優先直接回檔案,/api/... 沒有對應檔案時才進 Worker,學習階段容易搞混誰先跑。我們明確寫成「程式先看」。
public/index.html 用 fetch("/api/messages")。因為頁面與 API 同一個來源,不必加 CORS。第 2 週仍要記得:來源不同時,標頭才要登場。
常見錯誤
- 把這個原型佈署給朋友使用,隔天留言不見,或有時看得到有時看不到。那是白板的特性,不是客人操作錯。
id用陣列長度,刪除中間一筆後又用長度當新編號,可能撞號。案例改用只增不減的nextId。- 刪除時沒找到仍回
200。呼叫的人會以為刪掉了。 - 在
run_worker_first模式裡忘記把非 API 交給ASSETS,首頁變成 404。
自我檢查
- 列出兩條「記憶體陣列不能當正式資料庫」的理由。
run_worker_first改變了誰先處理請求?- 你的三扇門各對應哪個方法與路徑?
通過測驗再進第 3 週。下一週開始,留言會寫進 D1。