# 第六堂:串接 API 之術 ## 開課提醒 1. 錄影 2. **互動簡報連結**:<https://hexschool.github.io/2026-backend-slides/#/course/js-live> 3. 今天會用到一個外部 API(LiveJS 電商 API),請先到 <https://livejs-api.hexschool.io/> 註冊取得 API Path 與 Token 4. 請先 **fork** 本週作業專案:<https://github.com/hexschool/js-camp-week6>,再把你的 fork clone 下來、`npm install` ## 今日上課知識點 今天的內容分成三大區塊: ### 🌐 區塊一:HTTP 觀念 為什麼要 API、HTTP 是什麼、RESTful、狀態碼 ### ⚡ 區塊二:fetch 怎麼用 非同步 + Promise、async/await、fetch GET、response 物件、錯誤處理 ### 🛒 區塊三:真實場景實戰 環境變數、LiveJS、POST/PATCH/DELETE --- ## 今天的學習地圖 先別被上面的清單嚇到。今天看起來內容很多,但其實**核心只有兩件事**: 1. **理解非同步** — 為什麼 fetch 一定要 await 2. **學會 fetch 的兩個 pattern** — GET 一個、POST 一個(PATCH 跟 DELETE 都是 POST 的變化型) 其他的東西(HTTP、RESTful、狀態碼、.env)都是「順便認識的環境知識」。 這些觀念**現在聽過有印象就好**,作業卡住的時候再回來翻講義對照,不用一次背起來。 > 把今天當成**一張地圖**,不是一張考卷。 --- ## 你每天都在用 API(但你不知道) **你每天都在串 API**,只是不知道而已。Chrome 按 `F12` → Network → 逛任何網站,都會看到上百筆請求: | 你做的事 | 背後的 API 請求 | |---|---| | 在蝦皮搜尋商品 | `GET https://shopee.tw/api/v4/search/items?q=咖啡` | | 滑 IG 動態 | `GET https://i.instagram.com/api/v1/feed/timeline` | | 看 YouTube 影片 | `GET https://www.youtube.com/youtubei/v1/player` | | 查 Google Maps 路線 | `GET https://maps.googleapis.com/maps/api/directions/json` | | 查天氣 | `GET https://opendata.cwa.gov.tw/api/v1/...` | 每次回應都帶一個三位數字(**狀態碼**),最常見是 `200`(成功)。 > 今天目標:**從程式碼發出這些請求、拿回資料來用**。 --- ## 為什麼需要 API 前五堂資料都寫死在程式碼: ```jsx const products = [ { name: '拉麵', price: 180 }, { name: '炒飯', price: 150 } ]; ``` 問題: - 蝦皮幾百萬筆商品,能全部寫死嗎? - 上架新商品要工程師重新上線? - 昨天買的東西,今天怎麼還看得到? 資料其實放在**資料庫**,網站要用就跟資料庫要。但網站不能直接碰資料庫(危險、語言不通),中間需要「翻譯員」= **API**。 > API = Application Programming Interface,「程式跟程式之間的服務生」 ### 餐廳比喻(全課通用) | 角色 | 對應 | |---|---| | 客人(你) | 你寫的程式(client,前端) | | 服務生 | API | | 廚房 | Server(伺服器) | | 食材倉庫 | 資料庫 | 你不會自己衝進廚房 — 跟服務生說「我要拉麵」,服務生去廚房拿回來。API 就是這個服務生。 --- ## HTTP — 客人跟服務生的對話規則 兩台電腦講話也要有共同語言 = **HTTP**(HyperText Transfer Protocol)。拿德文在拉麵店點餐會沒人聽懂,HTTP 就是大家都聽得懂的那套規則。 每次對話 = 兩個步驟: - **Request(請求)** — 客人說「我要 XX」 - **Response(回應)** — 服務生回「這是 XX」或「沒有 XX」 每一次串 API = 一次「請求 + 回應」。 --- ## HTTP 方法 — 你要服務生做什麼 HTTP 五個常用動作: | 方法 | 意思 | 餐廳比喻 | |---|---|---| | `GET` | 拿資料 | 「給我看菜單」 | | `POST` | 送資料(新增) | 「我要下單:拉麵 + 煎餃」 | | `PUT` | 整份取代 | 「整單重來,換成炒飯」 | | `PATCH` | 改部分 | 「炒飯改成炒麵就好」 | | `DELETE` | 刪除 | 「取消訂單」 | ### PUT vs PATCH 的差別 用「改個資」最好懂。假設個資有 10 個欄位,只想改電話: | 方法 | 做法 | |---|---| | **PUT** | 10 個欄位全部重填一次寄回去,server 整份覆蓋 → 整份履歷重寄 | | **PATCH** | 只寄「電話」一個欄位,其他不動 → 一張便利貼 | > 大多數情況用 **PATCH**,省、又安全。 ### RESTful API 是什麼 一種 API 設計風格,核心觀念: > **網址只描述「資源」,動作交給 HTTP 方法表達。** 以購物車 `/carts` 為例: ``` GET /carts → 看購物車有什麼 POST /carts → 加東西到購物車 PATCH /carts → 改購物車裡的數量 DELETE /carts → 清空購物車 DELETE /carts/123 → 刪除 id 為 123 的那一筆 ``` 網址相同、方法不同 = RESTful 精神。 --- ## 同步 vs 非同步 學 API 的關鍵觀念。 | | 同步 | 非同步 | |---|---|---| | 行為 | 排隊,做完一件才做下一件 | 給你號碼牌,等好了再來換 | | 比喻 | 傻站在櫃台等拉麵 | 點完回座位滑手機,廣播叫號再去拿 | ```jsx console.log('1'); console.log('2'); console.log('3'); // 一定按順序印出 1, 2, 3(同步) ``` 為什麼網路要非同步?因為**等的時間很長**(幾秒起跳),JavaScript 不可能傻等什麼都不做。**所有網路請求都是非同步**。 ### 直接 demo 給你看 ```jsx const result = fetch('https://jsonplaceholder.typicode.com/users'); console.log(result); ``` 印出來: ``` Promise { <pending> } ``` 這就是 JavaScript 給你的「號碼牌」: > 「資料還在路上,先給你一個盒子,等資料到了會裝進去。」 盒子 = **Promise**,`<pending>` = 還沒裝好。 ### Promise 是什麼 > **Promise = 一個「等一下會裝著資料的盒子」** 你只要記: - 看到 Promise = 資料還沒到,等一下會到 - 要用 `await` 打開盒子拿值 --- ## await 跟 async ### 為什麼會拿到 pending? **JavaScript 不會停下來等你**。fetch 發出後立刻執行下一行,資料還沒回來,當然只拿到還沒裝好的盒子。 ### await — 打開盒子拿值 > **`await` = 叫 JavaScript 停下來,等盒子裝好,把裡面的值拿出來** ```jsx const result = await fetch('https://jsonplaceholder.typicode.com/users'); console.log(result); // 這次印出來會是真的 Response 物件,不是 Promise pending 了 ``` 但直接這樣寫會報錯: ``` SyntaxError: await is only valid in async functions ``` ### async — 給函式一個「我會等」的標籤 > **函式裡用了 await → 前面就要加 async** ```jsx async function getUsers() { // 1. 等 fetch 把資料拿回來 const response = await fetch('https://jsonplaceholder.typicode.com/users'); // 2. 印出結果 console.log(response); } getUsers(); // 別忘了呼叫 ``` > `async` = 函式門口的招牌「本店有等待業務」。 記三件事: 1. 看到 Promise → 用 `await` 打開 2. 用了 `await` → 函式前加 `async` 3. 別忘了呼叫函式(宣告 ≠ 執行) --- ## fetch 取得資料(GET) 練習 API:**JSONPlaceholder**(免註冊、免 key、乾淨 JSON) 網址:`https://jsonplaceholder.typicode.com/users` ### 基本範例 ```jsx async function getUsers() { const response = await fetch('https://jsonplaceholder.typicode.com/users'); const data = await response.json(); return data; } ``` 三行 code,兩個坑: ### ⚠️ 第一個坑:response.json() 也要 await ```jsx // ❌ 沒加 await async function getUsers() { const response = await fetch('https://jsonplaceholder.typicode.com/users'); const data = response.json(); // ← 漏 await return data; } ``` `.json()` **也是非同步**,它回傳的是 Promise,所以也要 await 打開。 ```jsx // ✅ 兩個 await 都要有 async function getUsers() { const response = await fetch('https://jsonplaceholder.typicode.com/users'); const data = await response.json(); return data; } ``` > 記法:**fetch 跟 json 兩兄弟,一個都不能少 await** ### ⚠️ 第二個坑:函式要 return | 寫法 | 結果 | |---|---| | ❌ 函式裡只 `console.log(data)`,沒 return | 呼叫端拿到 `undefined` | | ✅ 函式裡 `return data` | 呼叫端拿到真實資料 | ```jsx async function getUsers() { const response = await fetch('https://jsonplaceholder.typicode.com/users'); const data = await response.json(); // console.log(data) 印得出來,但呼叫端拿到 undefined return data; // ← 必須 return 才會把資料交給外面 } ``` > day3 的老觀念:**`console.log` ≠ `return`**。 ### 用 await 接結果 `getUsers()` 是 async function → 回傳 Promise → 呼叫端**也要 await**: ```jsx async function main() { const users = await getUsers(); console.log(users); console.log(users[0].name); } main(); ``` > **規則**:呼叫 async function 的結果,要 await 才能拿到真實資料。 ### 結合 day5 的陣列方法 拿到 users 後就是普通陣列,`map` / `filter` / `find` 全部照用: ```jsx async function showAllUserNames() { const users = await getUsers(); const lines = users.map(function(user) { return `${user.name} - ${user.email}`; }); console.log(lines); } showAllUserNames(); ``` > fetch 把資料拉回來,剩下的就是 day5 已經會的東西。 ### 完整可執行版本 組合起來,可以 `node 檔名.js` 直接跑: ```jsx // 1. 宣告非同步函式去拉資料 async function getUsers() { const response = await fetch('https://jsonplaceholder.typicode.com/users'); const data = await response.json(); return data; } // 2. 用另一個非同步函式接結果並處理 async function main() { const users = await getUsers(); console.log(`一共有 ${users.length} 位使用者`); // 用 map 把每筆資料轉成想要的格式 const lines = users.map(function(user) { return `${user.name} - ${user.email}`; }); console.log(lines); } // 3. 別忘了呼叫 main(); ``` 執行結果: ``` 一共有 10 位使用者 Leanne Graham - Sincere@april.biz Ervin Howell - Shanna@melissa.tv Clementine Bauch - Nathan@yesenia.net ... (以下省略) ``` > 這個範本看熟,後面 LiveJS 的 `getProducts` 幾乎就是一樣的 pattern。 --- ## response 物件裡到底有什麼? 除了 `.json()`,response 還有什麼?印出來看看: ```jsx async function inspectResponse() { const response = await fetch('https://jsonplaceholder.typicode.com/users'); console.log(response.status); // 200 console.log(response.ok); // true console.log(response.statusText); // 'OK' console.log(response.headers); // Headers 物件 } inspectResponse(); ``` `response` = fetch 給你的「整個信封」。內容要用 `.json()` 拿,其他重要欄位: | 欄位 | 用途 | |---|---| | `response.status` | 狀態碼數字(例如 200、404、500) | | `response.ok` | 布林值,**2xx 是 true、其他是 false** | | `response.statusText` | 狀態的文字說明(例如 'OK'、'Not Found') | | `response.headers` | server 回應的 headers 資訊 | | `response.json()` | 把 body 解析成 JSON(要 await) | > `response.ok` 是判斷請求成功的最快方法,比自己 `response.status === 200` 更省力。 ### HTTP 狀態碼五大類 `response.status` = **狀態碼**,三位數,告訴你這次處理結果。記**第一個數字**就好: | 類別 | 意思 | 餐廳比喻 | 常見代碼 | |---|---|---|---| | `1xx` | 收到了,正在處理 | 「點餐單收到,廚房準備中」 | 100 | | `2xx` | **成功** | 「您的餐點來了」 | 200 OK / 201 Created | | `3xx` | 改去別的地方 | 「我們搬家了,請去隔壁那家」 | 301 / 304 | | `4xx` | **你寫錯了** | 「你點的菜單上沒有」「你沒帶會員卡」 | 400 / 401 / 403 / 404 | | `5xx` | **廚房爆炸** | 「廚房失火了,今天不能出餐」 | 500 / 502 / 503 | 必記清單: | 代碼 | 意思 | |---|---| | **200** | 成功 | | **201** | 新增成功(POST 常回這個) | | **400** | 請求格式錯 | | **401** | 沒登入 | | **404** | 找不到(網址打錯) | | **500** | Server 自己出包 | > 簡單記法:**4 開頭是你的錯,5 開頭是別人的錯** --- ## 錯誤處理的第一道防線:`response.ok` 真實世界 URL 會打錯、server 會掛、權限會出包。**今天只學一件事 — 檢查 `response.ok`**。 ### fetch 沒爆,但 server 回 4xx / 5xx 最常見情境:路徑打錯,server 回 404: ```jsx async function inspectBadPath() { const response = await fetch('https://jsonplaceholder.typicode.com/notfound'); console.log(response.status); // 404 console.log(response.ok); // false // fetch 沒有丟錯誤,response 拿得到,只是 status 是 404 } inspectBadPath(); ``` 重點:**fetch 沒有丟錯誤**,response 拿得到,只是 status 是 404。檢查 `response.ok` 就能判斷成功或失敗。 > 比喻:包裹寄達了(fetch 成功),收件人拒收(status 404)。 ### 加上 response.ok 防護 ```jsx async function getUsersSafe() { const response = await fetch('https://jsonplaceholder.typicode.com/users'); // 檢查 server 是不是回成功 if (!response.ok) { console.error('Server 回錯誤:', response.status); return; } const data = await response.json(); return data; } ``` 一行 `if (!response.ok)` 就把「server 回錯誤」擋掉了。 > `response.ok` 是布林值,**2xx 時 true,其他都 false**。 ### 還有另一種錯誤:連線層級錯誤 網路整個斷、domain 不存在時,fetch 會**直接丟例外**,連 `response` 都拿不到。要擋這種錯誤要用 `try / catch`。 **今天不在主軸**,延伸研究章節有簡介,作業只要有檢查 `response.ok` 就過關。 --- ## 中場休息 --- ## LiveJS 電商 API 切換到真實 API:六角學院的練習用電商 API,產品 / 購物車 / 訂單一應俱全。 ### 註冊取得 API Path 1. 開 <https://livejs-api.hexschool.io/> → 註冊 → 登入 2. 建立你的 **API Path**(隨便取,例如 `gonsakon`) 3. 複製你的 **API Key**(一串亂碼) > **小提醒**:今天用的 customer 端點(products / carts)**不需要 token**,只要 API Path。API Key 之後管理員 API 會用到。 **Swagger 文件**:<https://hexschool.github.io/hexschoolliveswagger/> — 列出所有端點 / 參數 / 回傳格式,工程師日常翻的就是這種東西。 ### Fork 本週作業專案 整堂課的 code 都寫在作業專案裡,現在 **fork 一份**到你的帳號: 1. 開 <https://github.com/hexschool/js-camp-week6> → 右上角 **Fork** → 確認建立 2. clone 你的 fork: ```bash git clone https://github.com/你的帳號/js-camp-week6.git cd js-camp-week6 npm install ``` > **為什麼 fork 不直接 clone?** 你要把作業 push 回自己的 repo(繳交時貼的是你 fork 的連結)。直接 clone 原始 repo 沒有 push 權限。 專案結構: ``` js-camp-week6/ ├── homework.js ← 你要寫的地方(空框架 + TODO 註解) ├── test.js ← 自動測試(不要改) ├── .env ← 等下自己建立 ├── package.json └── README.md ``` ### 設定 .env 在專案根目錄建立 `.env`: ``` API_PATH=你註冊的 path API_KEY=你的 token ``` **為什麼要 .env?** - API Key 是秘密,**寫死在 code 跟著 push GitHub 會被盜** - 把秘密放 `.env`、程式碼用 `process.env.API_PATH` 讀取 - repo 公開也不外洩 > 作業已經幫你處理好:`homework.js` 有 `require("dotenv").config()`、`.gitignore` 也把 `.env` 列進去。你只要建檔填兩行。 ### 第一個 LiveJS 範例:填 `getProducts()` 打開 `homework.js` 找到 `getProducts()` 空框架,跟 `getUsers` 範例幾乎一樣,差別只有兩個: | 差別 | 說明 | |---|---| | 網址變長 | 用樣板字串把 `API_PATH` 塞進去 | | 回傳結構 | JSONPlaceholder 直接給陣列,LiveJS 包在 `data.products` | ```jsx async function getProducts() { const response = await fetch( `${BASE_URL}/api/livejs/v1/customer/${API_PATH}/products` ); const data = await response.json(); return data.products; } ``` > API 文件決定資料藏在哪一層。看到 `data.products` 就是「先拿 data、再從 data 拿 products」。 ### 填完跑跑看 ```bash npm start ``` `runTests()` 會自動呼叫 `getProducts()`,預期輸出: ``` --- 任務一:基礎 fetch --- getProducts: 成功取得 6 筆產品 ``` 看到筆數 = 串接成功 🎉。看到 `undefined` 最常見原因: - `.env` 沒設好 - `data.products` 寫成 `data` > 剩下六個函式都在同一個 `homework.js`,格式一樣 — 空框架等你填。下面會帶你寫。 #### 💡 除錯技巧:函式裡塞 `console.log` `npm test` 跑 15 題太慢。除錯時直接在函式裡塞 log,搭 `npm start` 跑: ```jsx async function getProducts() { const response = await fetch(...); const data = await response.json(); console.log(data); // ← 暫時加這行看看 data 長什麼樣 return data.products; } ``` `runTests()` 會自動呼叫 `getProducts()`,log 就會被觸發。除錯完刪掉。 **用法**:卡住就一層層往下印,看哪一層變 `undefined`。 ```js console.log(response); // fetch 回了什麼 console.log(data); // parse 完是什麼 console.log(data.products); // 路徑對不對 ``` ### 填 `getCart()` 跟 `getProducts` 一模一樣的 pattern,差別只有: | 差別 | 說明 | |---|---| | URL | 結尾從 `/products` 改成 `/carts` | | 回傳 | 包成 `{ carts, total, finalTotal }`(`total` = 小計、`finalTotal` = 折扣後)| ```jsx async function getCart() { const response = await fetch( `${BASE_URL}/api/livejs/v1/customer/${API_PATH}/carts` ); const data = await response.json(); return { carts: data.carts, total: data.total, finalTotal: data.finalTotal }; } ``` `npm test` → `getProducts` + `getCart` 兩組綠燈。 --- ## fetch 寫入資料(POST) 之前都是「拿資料」(GET),現在要「送資料過去」(POST)。GET 像明信片(只寫地址),POST 像包裹(地址 + 包裹內容)。 POST 要帶內容,所以 `fetch` 要多傳第二個參數,裡面有 `method` / `headers` / `body` 三個欄位。 ### 回到 homework.js:把 `addToCart` 填完 找到 `addToCart(productId, quantity)` 的空框架,填進去: ```jsx async function addToCart(productId, quantity) { const response = await fetch( `${BASE_URL}/api/livejs/v1/customer/${API_PATH}/carts`, { // method: 用哪個 HTTP 方法(預設是 GET,POST 要明寫) method: 'POST', // headers: 告訴 server「我寄的是 JSON 格式」。POST 沒加這行 9 成會壞 headers: { 'Content-Type': 'application/json' }, // body: 要送的資料。物件不能直接傳,要用 JSON.stringify 變字串 // ⚠️ LiveJS 的坑:body 要在外面再包一層 { data: ... } body: JSON.stringify({ data: { productId, quantity } }) } ); const data = await response.json(); return data; } ``` 三個要點直接寫在註解裡: - **`method: 'POST'`** — 不寫的話 fetch 預設是 GET - **`Content-Type: application/json`** — 沒加 server 不知道怎麼解析你的內容 - **`JSON.stringify(...)`** — 網路只能傳字串,物件要先「壓平」才能寄出去 - **`{ data: { ... } }`** — LiveJS 特殊規定,其他 API 不一定要這樣包,看 API 文件決定 存檔後跑 `npm test`,看到 `addToCart › 應回傳物件 ✓` 就過關。 ### 回到 homework.js:把 `getProductsSafe()` 填完 `getProducts` + `response.ok` 檢查 + 統一回傳格式。 | 結果 | 回傳格式 | |---|---| | ✅ 成功 | `{ success: true, data: 產品陣列 }` | | ❌ 失敗 | `{ success: false, error: '錯誤訊息' }` | ```jsx async function getProductsSafe() { const response = await fetch( `${BASE_URL}/api/livejs/v1/customer/${API_PATH}/products` ); // 先擋 4xx / 5xx if (!response.ok) { return { success: false, error: `HTTP ${response.status}` }; } const data = await response.json(); return { success: true, data: data.products }; } ``` > `error` 字串自由,看得懂就好(`HTTP 404` / `server 500` 都行)。 `npm test` → 任務一三題全綠。 > 剩下的 `updateCartItem` / `removeCartItem` / `clearCart` 都是 `addToCart` 的變化型 — `method` 換成 `'PATCH'` 或 `'DELETE'`、`body` 內容換掉、DELETE 不用 body。**這三題留給你自己挑戰**,卡住的話回頭看 Swagger 文件或作業 README。 --- ## 作業導讀 ### Fork 作業專案 原始 repo:<https://github.com/hexschool/js-camp-week6> 1. 開上面的連結,右上角點 **Fork** 建一份到你的帳號下 2. clone 你自己的 fork: ```bash git clone https://github.com/你的帳號/js-camp-week6.git cd js-camp-week6 npm install ``` 繳交作業時貼的是**你 fork 的 repo 連結**,不是原始 repo。 ### 作業專案結構 ``` js-camp-week6/ ├── homework.js ← 你要寫的地方 ├── test.js ← 自動測試(不要改) ├── .env ← 你的設定(自己建立) ├── package.json ← 套件清單 └── README.md ← 作業說明 ``` ### 三個指令 ```bash npm install # 第一次先裝套件(dotenv 跟 jest) npm start # 跑你的程式看輸出 npm test # 跑完整 Jest 測試看通過幾個 ``` --- ## 自己研究的關鍵字 本堂主軸:`await` + `response.ok`。其他延伸觀念 | 關鍵字 | 用途 | |---|---| | `Promise` | 什麼是 Promise 寫法 | | `try / catch / finally` | 接住 fetch 連線層級錯誤 | | `throw new Error()` | 自己丟錯誤 | | HTTP `Authorization` header | 帶 token 的 API(下堂課會用到) | --- ## 本週任務 必做: 1. [第六堂主線任務 — 電商 API 串接練習](https://rpg.hexschool.com/#/training/12063182914161572765/board/content/12063182914161572766_12063182914161572788?tid=12063182914167567607) 選做: 1. 每日任務 2. 課程筆記分享或延伸文章 ## 正課結束,下方為加碼環節 ## AI 實驗室 * 壓軸 ## Claude Code 從零上傳教學 * 從零開始 Claude Code ~ 30~45min * 錄影上傳 ## 這週新增的服務(4/3 — 4/10) ## 這週新增的服務(4/3 — 4/10) | Commit | 名稱 | 說明 | |--------|------|------| | `d0ea063` | **阿餘人設** | 260 行完整 skill 定義,設為專案預設人設 | | `491b0e1` | **帽子島看板桌布** | 自動渲染看板狀態為桌布 HTML,開機就能看進度 | | `9375a0d` | **早報 todo 自動推進** | 加上 `todo → in_progress` 規則,不用手動切狀態 | | `caba3c3` | **query-calendar CLI** | 獨立日曆查詢腳本,杜絕 primary 日曆漏查 | | `7aeff95` | **覆盤 skill** | session 結束後自動覆盤,找出協作摩擦點並產出改進建議 | 五項更新各補不同面向 — 人設定義、視覺化看板、工作流自動化、基礎工具修正、自我迭代機制。整個特助系統的縫隙在慢慢補起來。 ## 模擬面試:順序