前面兩篇我們把 CRUD 四個操作都寫出來了:
GET /tasks → 取得全部
POST /tasks → 新增
GET /tasks/{id} → 取得一筆
PUT /tasks/{id} → 修改
DELETE /tasks/{id} → 刪除
功能上其實已經可以用了,但如果現在把 Swagger 打開,會發現有幾個地方有點可惜:
每個 API 旁邊都沒有說明,看不出來它在做什麼
Swagger 只知道我們「會收到什麼」(@api.expect),但不知道我們「會回傳什麼」
回傳的東西是我們自己組出來的字典,格式全靠自己記得要一致
今天就把這些整理起來,讓它比較像一個完整的 API。
Day 13 我們用 api.model 描述了「收到的資料」長什麼樣子:
task_model = api.model('Task', {
'name': fields.String(required=True, ...),
'completed': fields.Boolean(required=True, ...),
})
這是給 @api.expect 用的,也就是「輸入」。
但我們回傳的資料其實多了一個 id:
{
"id": 1,
"name": "洗碗",
"completed": false
}
所以「輸入」和「輸出」其實長得不一樣。
新增的時候,id 是後端自己給的,不應該叫使用者傳進來。
所以我們再建立一個專門描述「輸出」的 model:
task_response = api.model('TaskResponse', {
'id': fields.Integer(readonly=True, description='作業編號'),
'name': fields.String(description='作業名稱'),
'completed': fields.Boolean(description='是否完成'),
})
readonly=True 的意思是「這個欄位是後端產生的,使用者不用填」,Swagger 文件上也會標示出來。
有了「輸出的 model」之後,就可以用 Flask-RESTX 的 marshal 功能。
marshal 可以理解成:照著 model 把要回傳的資料整理成統一的格式。
用法是掛在 method 上面:
@api.marshal_with(task_response)
→ 回傳一筆資料
@api.marshal_list_with(task_response)
→ 回傳一個清單
@api.marshal_with(task_response, code=201)
→ 回傳一筆資料,而且狀態碼是 201
加上去之後有兩個好處:
第一,Swagger 文件上會顯示這個 API 會回傳什麼格式。
第二,就算我們的字典裡多了一些不該傳出去的欄位(例如之後可能會有密碼、備註),只要 model 裡面沒寫,就不會被回傳出去。
這一點其實蠻重要的,因為 API 最怕的就是不小心把不該給的資料傳出去。
Flask-RESTX 有一個很方便的地方:
它會自動把 method 的 docstring 當成 Swagger 上的說明。
也就是我們只要寫:
def get(self):
"""取得所有作業"""
return tasks
Swagger 上的 GET /tasks 旁邊就會出現「取得所有作業」。
另外也可以用 @api.response() 補充這個 API 可能會回什麼狀態碼:
@api.response(404, '找不到作業')
這只是文件上的說明,不會影響程式行為,但會讓看文件的人清楚很多。
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',
description='30 天鐵人賽練習用的作業管理 API',
)
# 輸入:使用者要傳進來的資料
task_input = api.model('TaskInput', {
'name': fields.String(required=True, description='作業名稱', example='洗碗'),
'completed': fields.Boolean(required=True, description='是否完成', example=False),
})
# 輸出:API 回傳的資料
task_response = api.model('TaskResponse', {
'id': fields.Integer(readonly=True, description='作業編號'),
'name': fields.String(description='作業名稱'),
'completed': fields.Boolean(description='是否完成'),
})
tasks = []
next_id = 1
def find_task(task_id):
for task in tasks:
if task["id"] == task_id:
return task
api.abort(404, f"找不到 id 為{task_id} 的作業")
@api.route('/tasks')
class TaskList(Resource):
@api.marshal_list_with(task_response)
def get(self):
"""取得所有作業"""
return tasks
@api.expect(task_input)
@api.marshal_with(task_response, code=201)
def post(self):
"""新增一筆作業"""
global next_id
data = api.payload
task = {
"id": next_id,
"name": data.get("name"),
"completed": data.get("completed", False),
}
tasks.append(task)
next_id += 1
return task, 201
@api.route('/tasks/<int:task_id>')
@api.param('task_id', '作業編號')
@api.response(404, '找不到作業')
class TaskDetail(Resource):
@api.marshal_with(task_response)
def get(self, task_id):
"""取得指定編號的作業"""
return find_task(task_id)
@api.expect(task_input)
@api.marshal_with(task_response)
def put(self, task_id):
"""修改指定編號的作業"""
task = find_task(task_id)
data = api.payload
task["name"] = data.get("name", task["name"])
task["completed"] = data.get("completed", task["completed"])
return task
@api.response(204, '刪除成功')
def delete(self, task_id):
"""刪除指定編號的作業"""
task = find_task(task_id)
tasks.remove(task)
return '', 204
if __name__ == '__main__':
app.run(debug=True)
比起前兩篇,主要多了這些:
Api(…) 補上 description
→ Swagger 首頁會多一段說明(title 和 version 在 Day 12 已經加過了)
task_input / task_response
→ 把輸入和輸出分開描述
@api.marshal_with / @api.marshal_list_with
→ 統一回傳格式,Swagger 也會顯示回傳長什麼樣
docstring(每個 method 下面那一行文字)
→ 變成 Swagger 上的說明
@api.param('task_id', '作業編號')
→ 說明路徑參數是什麼
@api.response(404, '找不到作業')
→ 說明這個 API 可能會回 404
程式的邏輯其實沒有改,改的都是「怎麼描述這個 API」。
重新啟動後打開
http://127.0.0.1:5000/
Swagger 首頁,上面顯示 Task API 1.0 和描述,下面五個 API 都有中文說明
點開任一個 API,右邊的 Responses 區塊現在會顯示回傳的格式,最下面也會多出 Models 區塊,可以看到 TaskInput 和 TaskResponse。
Swagger 最下方的 Models 區塊,展開 TaskResponse 可以看到三個欄位
既然是「完成第一個 API」,就把五個操作完整跑一次:

現在把終端機的 Flask Server 關掉(Ctrl + C),再重新執行:
python app.py
然後打開 GET /tasks。
會看到:
[]
剛剛新增的資料全部不見了。
重新啟動 Server 之後,GET /tasks 又變回空陣列
因為我們的資料是存在:
tasks = []
這是一個 Python 的變數,存在程式執行時的記憶體裡。
程式結束,記憶體被釋放,資料自然就不見了。
除了重開會消失之外,還有一個問題是:這種寫法只有「這個程式自己」知道這些資料,沒辦法給其他程式使用,之後要部署到正式環境時通常也不會只跑一個程序,資料會對不起來。
用 task_input / task_response 分開描述輸入和輸出
用 marshal 統一回傳格式
用 docstring、@api.param、@api.response 讓 Swagger 文件更清楚
到這裡,第一個 API 算是完成了。
不過最後也發現了一個很現實的問題:資料只要重開就不見。
下一篇就來看看為什麼需要資料庫,以及資料庫到底幫我們解決了什麼問題。