# 第六堂:串接 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 結束後自動覆盤,找出協作摩擦點並產出改進建議 |
五項更新各補不同面向 — 人設定義、視覺化看板、工作流自動化、基礎工具修正、自我迭代機制。整個特助系統的縫隙在慢慢補起來。
## 模擬面試:順序