Learning Objectives
- 說明 API 與回傳 HTML 的網頁有什麼不同,以及為什麼需要 API
- 用複數名詞設計資源網址,選對 HTTP 方法,說明什麼是冪等
- 選對 200、201、204、400、404、409、422 這幾個狀態碼
- 用 jsonify、get_json、Location 標頭寫出筆記的 CRUD API,並用 API 工具與 curl 操作
誰會來呼叫 API
到目前為止,view 函數回傳的都是 HTML:瀏覽器收到後畫成畫面。可是想讀這些資料的不只有瀏覽器:
- 手機 App:畫面是 App 自己畫的,只需要資料
- 前端框架或頁面裡的 JavaScript:想更新畫面的一小塊,不想換整頁(第 13 課)
- 別的程式:排程腳本、其他網站的伺服器、AI Agent
這些呼叫者要的是結構化的資料,不是版面。所以另外開一組網址,回傳 JSON(JavaScript Object Notation,一種文字格式的資料,Python 的字典和串列可以直接對應)。這組網址就是 API(Application Programming Interface)。
REST 是一種常見的設計風格:把資料看成一個個資源(resource),每個資源有自己的網址,用 HTTP 方法表達要做什麼。REST 是風格,不是標準,沒有人會檢查你「夠不夠 REST」;重點是讓別人看到網址和方法就猜得到它做什麼。
逐步看:一個會回 JSON 的 API
和前幾課一樣,client = app.test_client() 扮演呼叫者。注意 create_note 回傳的三個東西,再看 client 那邊怎麼用 Location 繼續下一個請求。
資源命名與 HTTP 方法
網址用名詞表示資源,複數表示一群,動作交給 HTTP 方法:
| 方法與網址 | 意思 | 成功時的狀態碼 |
|---|---|---|
GET /api/notes |
列出筆記 | 200 |
POST /api/notes |
新增一則 | 201,加上 Location |
GET /api/notes/3 |
讀第 3 則 | 200 |
PUT /api/notes/3 |
把第 3 則整筆換掉 | 200 |
PATCH /api/notes/3 |
只改第 3 則的部分欄位 | 200 |
DELETE /api/notes/3 |
刪除第 3 則 | 204,沒有內容 |
GET /api/courses/8/notes |
第 8 門課底下的筆記(巢狀) | 200 |
/api/getNote?id=3、/api/deleteNote/3 這種把動作寫進網址的做法,方法就失去意義了,別人也猜不到還有哪些動作。
冪等(idempotent):同一個請求送一次和送很多次,伺服器上的結果一樣。這在網路不穩時很實用:呼叫者沒收到回應,不知道伺服器處理了沒有,如果方法是冪等的,再送一次也不會出事。
依 RFC 9110,GET、PUT、DELETE 是冪等的,POST 不是。下面的動畫實際送兩次,比較 notes 的變化。
逐步看:同一個請求送兩次
每個請求都送兩次。請看輸出最右邊的 notes:哪一種方法送第二次時,伺服器上的資料又變了?
選對狀態碼
呼叫者的程式靠狀態碼決定下一步,所以「出錯還回 200」是常見的壞習慣:程式會以為成功了。這一課會用到的幾個:
| 狀態碼 | 名稱 | 什麼時候用 |
|---|---|---|
| 200 | OK | 成功,有內容 |
| 201 | Created | 新增成功,加上 Location |
| 204 | No Content | 成功,沒有內容(例如刪除) |
| 400 | Bad Request | 請求本身有問題,例如 JSON 語法錯誤 |
| 404 | Not Found | 這個資源不存在 |
| 405 | Method Not Allowed | 網址存在,但不接受這個方法(Flask 自動處理) |
| 409 | Conflict | 和現有資料衝突,例如標題重複 |
| 422 | Unprocessable Content(Flask 目前顯示舊名稱 Unprocessable Entity) | 格式對,但內容不合規則,例如標題空白 |
400 和 422 的界線不是每個 API 都一樣,有些 API 全部回 400。同一個 API 裡保持一致比選哪一個更重要。
逐步看:同一個端點的四種結果
四個請求依序走到不同的 return。下一課會把這些檢查寫得更完整,這裡先看狀態碼怎麼分。
在終端機用 curl 呼叫 API
API 工具做的事,在真實環境通常用 curl 或 Postman 這類工具。把上面的 app.py 存到自己的電腦,開兩個終端機,一個跑 flask --app app run,另一個照下面操作。
下面的輸出是實際執行的結果,日期和版本會依你的環境不同。指令是 macOS、Linux 的寫法;Windows 的 PowerShell 裡 curl 可能是別的指令,請寫 curl.exe,而且 JSON 裡的引號要另外跳脫,最簡單的做法是把內容存成檔案,用 -d @note.json 送出。
-
1
新增一則筆記
-X指定方法,-H加標頭,-d是內容,-i連回應標頭一起顯示。少了Content-Type: application/json,get_json()會回 415。 -
2
讀、改、刪
GET 是預設方法,不用寫
-X。DELETE成功時沒有內容,所以用-w把狀態碼印出來。
動手做:讀書會 API
補完三個地方,讓狀態碼、標頭與內容都正確。程式最下面的測試不要改。
想一想
要設計「刪除第 3 則筆記」的 API,哪一種寫法最符合 REST 風格?
下面的程式會印出什麼?
from flask import Flask, jsonify
app = Flask(__name__)
@app.post('/api/notes')
def create():
return jsonify(id=1), 201, {'Location': '/api/notes/1'}
r = app.test_client().post('/api/notes')
print(r.status_code, r.headers.get('Location'))
同一個 DELETE /api/notes/2 送兩次,第一次回 204,第二次回 404。這樣還算冪等嗎?
使用者新增筆記時,標題和自己的另一則筆記重複,你決定不允許。最適合回哪個狀態碼?
呼叫者送出 POST /api/notes 後網路斷了,沒收到回應。他可以直接再送一次嗎?
重點整理
- API 回傳 JSON 這類結構化的資料,給程式讀,不是給瀏覽器畫
- 網址用複數名詞表示資源,動作交給 HTTP 方法;同一個 API 的風格要一致
GET、PUT、DELETE是冪等的,POST不是;冪等看的是伺服器上的結果- 新增回 201 加
Location,刪除回 204,找不到回 404,衝突回 409,出錯不要回 200 jsonify產生 JSON 回應,request.get_json()讀請求內容
目前的 API 假設呼叫者都送對的東西:少了 title、型別不對、不是 JSON,或者打錯網址,回應都不夠好。下一課處理這些情況,再加上分頁。