sell整合實戰
api

用 Workers 設計 REST API

這是「前端 ↔ 後端」裡的後端那一半:用一個 Worker 檔案,像正規的 REST API 一樣回應 GET、POST、PUT、DELETE。

4CRUD 動詞
1個 Worker 檔
0ms冷啟動
CORS瀏覽器可直連
insights

我們要做什麼?

我們要在單一個 Cloudflare Worker 上做一個 REST API。所謂「REST API」(Representational State Transfer,表現層狀態轉換)其實就是前端跟後端「用 HTTP 要資料」的一套共同約定:你用一段網址路徑代表一種資源(例如 /api/users),再用一個 HTTP 方法(GET、POST、PUT、DELETE)表示你想對它做什麼。

整個後端就放在一個 Worker 裡。當一個請求進來,Worker 會讀兩樣東西——方法(method)跟路徑(path)——決定該跑哪一段處理程式,做完事,再回傳一個帶狀態碼的 JSON 回應。就這樣,連框架都不用。

schema整體架構

HTTP 請求

附上 CORS 標頭

瀏覽器前端 (fetch)

Worker REST API

路由器 (方法 + 路徑)

GET 處理函式

POST 處理函式

PUT 處理函式

DELETE 處理函式

JSON + 狀態碼

restaurant_menu

把它想成一家餐廳

網址路徑就像桌號(指哪一種資源),HTTP 方法就是你對服務生說的話:GET =「給我看菜單」、POST =「我要點這個」、PUT =「改一下我的餐點」、DELETE =「取消」。狀態碼則是服務生的回覆:200「好的給你」、201「已幫你下單」、404「我們沒有這個」。

account_tree

路由器怎麼做決定

在 Worker 內部,路由其實就是依據兩個從請求讀到的值來分流:request.method,以及 new URL(request.url) 取出的 pathname(路徑)。每一組「方法/路徑」對應到一個處理函式,每個處理函式都回傳一個帶有合適狀態碼的回應。

front_hand

OPTIONS — 預檢

瀏覽器的 CORS 安全檢查。最先處理它,回 204 加上 CORS 標頭,再談後面的事。

download

GET — 讀取

列出整批(/api/users)或拿單一筆(/api/users/:id)。回 200,找不到就回 404。

add_circle

POST — 新增

讀取 JSON 內容、驗證、建立資源。回 201 Created,內容不合法就回 400。

edit

PUT — 更新

用 :id 找到該筆並替換欄位。回 200、輸入不對回 400、找不到回 404。

delete

DELETE — 刪除

用 :id 刪掉該筆。刪掉了回 200,本來就沒有就回 404。

block

其他情況

不認識的路徑回 404;路徑存在但方法不對,回 405 Method Not Allowed(方法不允許)。

schema請求路由器

OPTIONS

GET

POST

PUT

DELETE

other

/api/users

/api/users/:id

收到請求

method?

204 + CORS 標頭

path?

新增 → 201 / 400

更新 → 200 / 400 / 404

刪除 → 200 / 404

405 方法不允許

列出 → 200

單筆 → 200 / 404

swap_vert

瀏覽器呼叫的逐步流程

當某個網域上的網頁去呼叫另一個網域的 API 時,瀏覽器會執行 CORS(Cross-Origin Resource Sharing,跨來源資源共用)——這是一條保護使用者、避免被偷偷跨站請求的規則。對某些請求,瀏覽器會先送出一個「預檢(preflight)」:一個 OPTIONS 請求,問 API「你同意我呼叫你嗎?」。只有當 API 回應正確的 Access-Control-* 標頭,瀏覽器才會送出真正的請求。

schema先預檢再 GET
"Worker API"瀏覽器"Worker API"瀏覽器CORS 預檢真正的請求OPTIONS /api/users204 + Access-Control-Allow-*GET /api/users依方法與路徑路由200 JSON + CORS 標頭
info

不是每個請求都會預檢

「簡單請求」(單純的 GET,或用基本內容類型的 POST)會跳過 OPTIONS 那一步。但只要你送出 Content-Type: application/json 或自訂標頭,瀏覽器就會預檢——這就是為什麼 JSON API 一定要處理 OPTIONS。

schema單一處理函式內部

成功

輸入錯誤

找不到

程式爆錯

處理函式執行

結果?

200 / 201 JSON

400 錯誤 JSON

404 錯誤 JSON

500 錯誤 JSON

附上 CORS 標頭

回傳給瀏覽器

construction

動手做

以下是完整後端(單一檔案)、對應的前端 fetch 範例,以及 wrangler 設定。Worker 用一個小小的記憶體陣列當作「假資料庫」,讓你專心看路由——要真正持久保存資料時,再換成 D1 或 KV。

  1. 建立 Worker 專案

    用 JavaScript 產生一個最單純的「Hello World」Worker,然後打開 src/index.js。

    bash
    npm create cloudflare@latest -- my-api
    cd my-api
  2. 寫 REST 路由器

    把 src/index.js 換成下面的後端程式碼。它會讀取方法與路徑、路由到對應的處理函式,並用 Response.json(...) 回傳正確狀態碼與 CORS 標頭。

  3. 先本機跑,再部署

    在 http://localhost:8787/api/users 測試,再發佈到公開的 *.workers.dev 網址。

    bash
    npx wrangler dev
    npx wrangler deploy
jssrc/index.js — REST API
// A single-file REST API on one Worker.

// CORS = Cross-Origin Resource Sharing(跨來源資源共用):
// 這些標頭讓「別的網域」的瀏覽器前端可以呼叫我們。
const CORS = {
  "Access-Control-Allow-Origin": "*", // 誰可以呼叫(* = 任何人;正式環境請鎖成你的網域)
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
  "Access-Control-Allow-Headers": "Content-Type",
};

// 一個小小的記憶體資料(重啟會清空——正式請改用 D1 / KV)。
let users = [
  { id: 1, name: "Ada" },
  { id: 2, name: "Linus" },
];
let nextId = 3;

// 用一個 helper 統一產生每個 JSON 回應:資料 + 狀態碼 + CORS 標頭。
function json(data, status = 200) {
  return Response.json(data, { status, headers: CORS });
}

export default {
  async fetch(request) {
    const { pathname } = new URL(request.url); // 例如 "/api/users/2"
    const method = request.method;             // 例如 "GET"

    // 1) CORS 預檢:瀏覽器在真正跨站請求前會先送 OPTIONS。
    if (method === "OPTIONS") {
      return new Response(null, { status: 204, headers: CORS });
    }

    // 2) 只服務我們的 API 路徑;其餘一律 404 Not Found(找不到)。
    if (!pathname.startsWith("/api/users")) {
      return json({ error: "Not found" }, 404);
    }

    // 3) 從 /api/users/:id 取出可有可無的 id。
    const parts = pathname.split("/").filter(Boolean); // ["api","users","2"]
    const id = parts[2] ? Number(parts[2]) : null;

    try {
      // GET /api/users → 列出全部(200 OK)
      if (method === "GET" && id === null) {
        return json(users, 200);
      }

      // GET /api/users/:id → 取單筆(200,找不到 404)
      if (method === "GET" && id !== null) {
        const user = users.find((u) => u.id === id);
        return user ? json(user, 200) : json({ error: "User not found" }, 404);
      }

      // POST /api/users → 新增(201 Created,輸入不合法 400 Bad Request)
      if (method === "POST" && id === null) {
        const body = await request.json();
        if (typeof body?.name !== "string") {
          return json({ error: "name is required" }, 400);
        }
        const user = { id: nextId++, name: body.name };
        users.push(user);
        return json(user, 201);
      }

      // PUT /api/users/:id → 更新(200 / 400 / 404)
      if (method === "PUT" && id !== null) {
        const user = users.find((u) => u.id === id);
        if (!user) return json({ error: "User not found" }, 404);
        const body = await request.json();
        if (typeof body?.name !== "string") {
          return json({ error: "name is required" }, 400);
        }
        user.name = body.name;
        return json(user, 200);
      }

      // DELETE /api/users/:id → 刪除(200,找不到 404)
      if (method === "DELETE" && id !== null) {
        const before = users.length;
        users = users.filter((u) => u.id !== id);
        return before === users.length
          ? json({ error: "User not found" }, 404)
          : json({ ok: true }, 200);
      }

      // 路徑存在,但方法不對 → 405 Method Not Allowed(方法不允許)。
      return json({ error: "Method not allowed" }, 405);
    } catch (err) {
      // JSON 內容壞掉或其他意外 → 400 Bad Request。
      return json({ error: "Invalid request" }, 400);
    }
  },
};
js前端 — 用 fetch() 呼叫 API
// 這段跑在瀏覽器裡。換成你部署後拿到的網址。
const API = "https://my-api.<your-subdomain>.workers.dev/api/users";

// GET:列出全部使用者
const res = await fetch(API);
console.log(res.status, await res.json()); // 200, [ {id:1,...}, ... ]

// POST:新增一筆(把 JSON 放在 body,並標明 Content-Type)
const created = await fetch(API, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "Grace" }),
});
console.log(created.status, await created.json()); // 201, { id: 3, name: "Grace" }

// PUT:更新 id = 3 那筆
await fetch(`${API}/3`, {
  method: "PUT",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "Grace H." }),
});

// DELETE:刪掉 id = 3 那筆
await fetch(`${API}/3`, { method: "DELETE" });
tomlwrangler.toml
name = "my-api"
main = "src/index.js"
compatibility_date = "2025-01-01"
database

記憶體資料不會留存

每個 Worker isolate(隔離環境)都有自己一份 users 陣列,而且過一段時間就會重置。拿來學路由很完美,但要存真資料就需要儲存層。可參考 D1 CRUD 教學,把這個 API 接到 SQL 資料庫。

school

重點概念

http

HTTP 方法

請求的「動詞」——GET 讀、POST 建立、PUT 更新、DELETE 刪除。用 request.method 讀到它。

link

URL 與路徑

new URL(request.url) 會解析網址。它的 pathname(例如 /api/users/2)告訴你對方想要哪個資源。

data_object

JSON 內容

POST 與 PUT 會在請求內容(body)裡帶資料。await request.json() 把它轉成 JavaScript 物件——記得一定要驗證。

check_circle

狀態碼

三位數的結果:2xx 成功、4xx 用戶端出錯、5xx 伺服器出錯。它讓前端不必解析文字就能判斷狀況。

tune

Response.json()

Workers 的便利函式:把物件序列化成 JSON 並自動設好 Content-Type。第二個參數可放 { status, headers }。

public

CORS

授權「別的網域」瀏覽器呼叫你的標頭。要處理 OPTIONS,並在每個回應加上 Access-Control-Allow-*。

fork_right

路由

把「方法 + 路徑」對應到一個處理函式。路由不多時,幾個 if 判斷就贏過任何框架;路由很多時,可改用 itty-router 或 Hono。

rule

冪等性

GET、PUT、DELETE 重複呼叫結果應一樣;POST 通常每次都會新建一筆。這會幫你決定該選哪個動詞。

最常用的狀態碼

  • 200 OK — 請求成功(GET、PUT、DELETE)
  • 201 Created — 成功建立了新資源(POST)
  • 204 No Content — 成功但沒有內容(很適合 OPTIONS 預檢)
  • 400 Bad Request — 用戶端送了不合法的資料
  • 404 Not Found — 該路徑 / id 沒有對應的資源
  • 405 Method Not Allowed — 路徑存在,但不支援這個動詞
  • 500 Internal Server Error — 你這邊的程式爆掉了
tips_and_updates

小提示與常見陷阱

report

最常見的 CORS 錯誤

如果你忘了處理 OPTIONS,或在錯誤回應上漏掉 CORS 標頭,瀏覽器就會擋下這次呼叫,主控台會出現讓人摸不著頭緒的「CORS error」——即使你的 Worker 其實有跑。請在「每一個」回應上都附上 CORS 標頭,不論成功或失敗。

lock

正式環境請鎖定來源

學習階段用 Access-Control-Allow-Origin: * 沒問題,但正式環境請改成你真正的前端網域(例如 https://app.example.com),這樣才只有你的網站能從瀏覽器呼叫這個 API。

讓 API 保持乾淨的好習慣

  • 永遠把 request.json() 包在 try/catch 裡——內容壞掉時要回 400,而不是讓 Worker 當掉
  • 信任任何欄位前都要先驗證;絕不要把未檢查的輸入寫進儲存層
  • 錯誤一律用一致的 JSON 形狀,例如 { "error": "訊息" },前端才能統一處理
  • 依「意圖」選動詞:讀取 = GET、新增 = POST、整筆替換 = PUT、刪除 = DELETE
  • 保留單一個 json() helper,讓狀態碼與 CORS 標頭只在一個地方設定
  • 當路由變多時,從一堆 if 改用 Hono 之類的路由函式庫,維持可讀性
好的 REST API 是「刻意無聊」的:路徑可預測、動詞可預測、狀態碼也可預測。