iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0

前面兩篇我們把 CRUD 四個操作都寫出來了:

GET /tasks → 取得全部
POST /tasks → 新增
GET /tasks/{id} → 取得一筆
PUT /tasks/{id} → 修改
DELETE /tasks/{id} → 刪除

功能上其實已經可以用了,但如果現在把 Swagger 打開,會發現有幾個地方有點可惜:

每個 API 旁邊都沒有說明,看不出來它在做什麼
Swagger 只知道我們「會收到什麼」(@api.expect),但不知道我們「會回傳什麼」
回傳的東西是我們自己組出來的字典,格式全靠自己記得要一致

今天就把這些整理起來,讓它比較像一個完整的 API。

問題 1:Swagger 看不出來 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 文件上也會標示出來。

問題 2:回傳格式靠自己記

有了「輸出的 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 最怕的就是不小心把不該給的資料傳出去。

問題 3: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」。

看看整理後的 Swagger

重新啟動後打開

http://127.0.0.1:5000/
https://ithelp.ithome.com.tw/upload/images/20260930/20183865ng6v99lS5g.png
Swagger 首頁,上面顯示 Task API 1.0 和描述,下面五個 API 都有中文說明

點開任一個 API,右邊的 Responses 區塊現在會顯示回傳的格式,最下面也會多出 Models 區塊,可以看到 TaskInput 和 TaskResponse。
https://ithelp.ithome.com.tw/upload/images/20260930/201838655IpFcbGlao.png
Swagger 最下方的 Models 區塊,展開 TaskResponse 可以看到三個欄位

完整跑一次 CRUD

既然是「完成第一個 API」,就把五個操作完整跑一次:

  1. POST /tasks 新增「洗碗」
  2. POST /tasks 新增「寫 Day 16」
  3. GET /tasks 應該看到兩筆
  4. PUT /tasks/1 把 completed 改成 true
  5. GET /tasks/1 確認改成功
  6. DELETE /tasks/2
  7. GET /tasks 只剩下一筆
    https://ithelp.ithome.com.tw/upload/images/20260930/20183865jSwulj9CfY.png
    完整跑完一輪之後,GET /tasks 的結果

但是有一個問題

現在把終端機的 Flask Server 關掉(Ctrl + C),再重新執行:

python app.py

然後打開 GET /tasks。

會看到:

[]

剛剛新增的資料全部不見了。
https://ithelp.ithome.com.tw/upload/images/20260930/20183865aJVCHPmb36.png
重新啟動 Server 之後,GET /tasks 又變回空陣列

因為我們的資料是存在:

tasks = []

這是一個 Python 的變數,存在程式執行時的記憶體裡。

程式結束,記憶體被釋放,資料自然就不見了。

除了重開會消失之外,還有一個問題是:這種寫法只有「這個程式自己」知道這些資料,沒辦法給其他程式使用,之後要部署到正式環境時通常也不會只跑一個程序,資料會對不起來。

今天把前面寫的 CRUD 整理成一個比較完整的 API:

用 task_input / task_response 分開描述輸入和輸出
用 marshal 統一回傳格式
用 docstring、@api.param、@api.response 讓 Swagger 文件更清楚

到這裡,第一個 API 算是完成了。

不過最後也發現了一個很現實的問題:資料只要重開就不見。

下一篇就來看看為什麼需要資料庫,以及資料庫到底幫我們解決了什麼問題。


上一篇
Day 15|CRUD:修改與刪除
下一篇
Day 17|為什麼需要資料庫?
系列文
從零開始的後端開發:用 Flask 實作 REST API,搞懂 API 與資料庫之間如何協作 共 18 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言