上一篇我們認識了 Swagger,也實際用 Swagger 測試了:
GET /tasks
按下 Execute 之後,Swagger 幫我們發出 GET Request,最後顯示 API 回傳的資料。
不過目前為止,我們做的都還是「取得資料」。
如果今天是要新增一筆作業,只說「我要新增作業」是不夠的,還要告訴後端:
要新增的作業叫什麼名字?完成了沒有?
也就是說,這次的 Request 需要帶著資料一起送出去。
那這些資料要放在 Request 的哪裡?後端又要怎麼拿到?
今天就來看看 Request Body 和 api.payload。
在 Day 3 我們有提到,Request 不是單純「傳資料」,而是向後端提出一個請求,而這個請求裡面可以帶著需要處理的資料。
常見放資料的位置有幾種,今天先認識兩個:
網址上(Query String)
例如:/tasks?completed=true
資料會直接出現在網址裡,常用在 GET 這種「查詢、篩選」的情況。
Request Body(請求主體)
資料放在 Request 的內容裡,不會出現在網址上。
新增、修改資料時(POST、PUT)通常會用這種方式。
今天我們要做的是「新增一筆作業」,所以會使用 Request Body。
我們之前在 Day 9 認識了 JSON,也知道 Web API 很常使用 JSON 來交換資料。
其實不只是後端回傳的 Response 可以是 JSON,前端送出的 Request Body 也可以是 JSON。
一個新增作業的 Request,大概可以想成這樣:
POST /tasks
Content-Type: application/json
{
"name": "寫 Day 13",
"completed": false
}
拆開來看:
POST /tasks → 我想要對作業這個資源做「新增」
Content-Type: application/json → 我這次帶的資料是 JSON 格式
{ … } → 這就是 Request Body,裡面是要新增的資料
Content-Type 可以先理解成「我這次傳的資料是什麼格式」,讓後端知道要怎麼讀這包資料。
如果是只用 Flask,我們會這樣拿:
from flask import request
data = request.get_json()
而我們現在使用的是 Flask-RESTX,它提供了一個更短的寫法:
api.payload
payload 可以理解成「這次 Request 帶進來的資料」。
api.payload 其實就是 Flask-RESTX 幫我們處理好的 request.get_json(),拿到的結果會是一個 Python 字典。
這裡可以跟 Day 9 一起對照著看:
進來的時候:JSON → Python 字典(api.payload)
出去的時候:Python 字典 → JSON(Flask 幫我們轉)
先不要想太多,我們讓 POST /tasks 把收到的資料原封不動回傳回來,確認我們真的拿到了資料。
app.py
from flask import Flask
from flask_restx import Api, Resource
app = Flask(__name__)
api = Api(app, title='Task API', version='1.0')
@api.route('/tasks')
class Tasks(Resource):
def get(self):
return {
"name": "Flask",
"completed": False
}
def post(self):
data = api.payload
return {"你傳進來的資料": data}, 201
if __name__ == '__main__':
app.run(debug=True)
這裡有兩個新東西:
第一個是 def post(self)
Day 11 有提到,Flask-RESTX 會用 method 的名字去對應 HTTP Method。
所以 /tasks 收到 GET Request 時執行 get(),收到 POST Request 時就會執行 post()。
第二個是 return 後面多了一個 201
return {“你傳進來的資料”: data}, 201
201 是 HTTP 狀態碼(Status Code),用來表示這次請求的結果。
常見的像是:
200 → OK,成功
201 → Created,成功而且建立了新資料
404 → Not Found,找不到
因為 POST 是新增資料,所以通常會回 201 而不是 200。
狀態碼在 Day 24 還會再講得更完整,今天先知道它是在表示「這次請求的結果」。
存檔後重新啟動:
python app.py
這時候 Swagger 上會多出一個:
POST /tasks
Swagger 首頁,/tasks 下面同時有 GET 和 POST 兩個 Method
但是點開 POST /tasks 之後會發現一件事:
它只有一顆 Try it out,沒有地方可以讓我們填要傳的 JSON。
因為我們還沒有告訴 Flask-RESTX:這個 API 預期會收到什麼樣的資料。
所以就算程式裡面有 api.payload,Swagger 也不知道要幫我們準備輸入框。
這時候就需要 api.model 和 @api.expect。
api.model 是用來描述「一筆資料有哪些欄位、每個欄位是什麼型別」。
先在 import 的地方多加一個 fields:
from flask_restx import Api, Resource, fields
然後建立 model:
task_model = api.model('Task', {
'name': fields.String(required=True, description='作業名稱', example='寫 Day 13'),
'completed': fields.Boolean(required=True, description='是否完成', example=False),
})
拆開來看:
api.model('Task', {...})
第一個參數 ‘Task’ 是這個資料模型在 Swagger 文件上顯示的名字。
‘name’: fields.String(…)
表示有一個欄位叫 name,型別是字串。
‘completed’: fields.Boolean(…)
表示有一個欄位叫 completed,型別是布林值(True / False)。
required=True → 這個欄位是必填
description → 這個欄位的說明,會顯示在 Swagger 上
example → Swagger 上會用它當範例值,測試的時候很方便
有了 model 之後,再把它掛到 post() 上面:
@api.route('/tasks')
class Tasks(Resource):
@api.expect(task_model)
def post(self):
data = api.payload
return {"你傳進來的資料": data}, 201
@api.expect(task_model)
可以理解成:「這個 API 預期收到的 Request Body,長得像 task_model 這樣。」
完整的 app.py:
from flask import Flask
from flask_restx import Api, Resource, fields
app = Flask(__name__)
api = Api(app, title='Task API', version='1.0')
task_model = api.model('Task', {
'name': fields.String(required=True, description='作業名稱', example='寫 Day 13'),
'completed': fields.Boolean(required=True, description='是否完成', example=False),
})
@api.route('/tasks')
class Tasks(Resource):
def get(self):
return {
"name": "Flask",
"completed": False
}
@api.expect(task_model)
def post(self):
data = api.payload
return {"你傳進來的資料": data}, 201
if __name__ == '__main__':
app.run(debug=True)
重新啟動後打開 http://127.0.0.1:5000/
點開 POST /tasks,這次會發現多了一塊 Example Value,而且裡面已經幫我們填好範例:
{
"name": "寫 Day 13",
"completed": false
}
點開 POST /tasks 之後,右邊出現 Example Value / Model 的畫面
按下 Try it out,就可以直接修改裡面的 JSON,例如改成:
{
"name": "洗碗",
"completed": false
}
再按 Execute。
Try it out 之後可以編輯 JSON 的輸入框
Swagger 會顯示 Response:
{
"你傳進來的資料": {
"name": "洗碗",
"completed": false
}
}
而且 Code 那一欄會是 201。
Execute 之後的 Response,可以看到 201 和回傳的資料
整個流程:
Swagger 填好 JSON 並按下 Execute
↓
發出 POST Request 到 /tasks,Request Body 帶著 JSON
↓
Flask-RESTX 找到 /tasks 對應的 Resource
↓
因為是 POST Request
↓
執行 post()
↓
api.payload 把 JSON 轉成 Python 字典
↓
回傳結果(Response)
@api.expect(task_model) 看起來很像「檢查」,但它預設其實不會擋。
也就是說,如果我們故意送一包這樣的資料:
{
"abc": 123
}
它還是會成功執行,然後回傳:
{
"你傳進來的資料": {
"abc": 123
}
}
故意傳錯誤的欄位,結果還是 201 的畫面
因為 @api.expect 預設只負責「文件上的說明」,讓 Swagger 知道要顯示什麼輸入框。
如果要真的擋下來,需要寫成:
@api.expect(task_model, validate=True)
加上 validate=True 之後,少填必填欄位就會直接回 400。
這部分屬於 Input Validation,Day 25 會專門再講,今天先知道有這件事就好,不然很容易以為「我明明有寫 model,為什麼亂傳也會過」。
今天我們讓 API 從「只會回傳資料」變成「可以接收資料」。
Request Body:Request 帶資料進來的地方,Web API 常用 JSON
api.payload:Flask-RESTX 幫我們把 Request Body 的 JSON 轉成 Python 字典
api.model:描述資料有哪些欄位、什麼型別
@api.expect:告訴 Flask-RESTX 和 Swagger,這個 API 預期收到什麼
不過現在有一個問題:
我們雖然收到了資料,但只是把它原封不動回傳回去,並沒有真的把它存起來。
所以現在新增完,再用 GET /tasks 去查,也查不到剛剛新增的東西。
下一篇就來把資料真的存起來,完成 CRUD 的前兩個:新增(POST)和查詢(GET)。