iT邦幫忙

2026 iThome 鐵人賽

DAY 15
0

上一篇完成了 CRUD 的前兩個:

POST /tasks → 新增
GET /tasks → 查全部
GET /tasks/{id} → 查其中一筆

今天要把剩下的兩個做完:

PUT /tasks/{id} → 修改
DELETE /tasks/{id} → 刪除

為什麼是 /tasks/{id},不是 /tasks?

修改和刪除都一定要先知道「要動的是哪一筆」。

如果只寫 /tasks,後端不會知道我們想改的是第 1 筆還是第 2 筆。

所以:

PUT /tasks/1 → 修改編號 1 的作業
DELETE /tasks/1 → 刪除編號 1 的作業

這也呼應 Day 6 說的:Path 指定 Resource,Method 表示要做什麼。

先把「找資料」抽出來

上一篇的 get() 裡面有一段找資料的程式:

for task in tasks:
    if task["id"] == task_id:
        return task

api.abort(404, ...)

今天 PUT 和 DELETE 也都需要先找到那一筆資料,如果每個地方都複製一次會很雜亂。

所以先把它抽成一個函式:

def find_task(task_id):
    for task in tasks:
        if task["id"] == task_id:
            return task

    api.abort(404, f"找不到 id 為{task_id} 的作業")

因為 api.abort() 會直接中斷這次的請求,所以只要順利執行到下一行,就代表資料一定有找到。

完整程式

app.py

from flask import Flask
from flask_restx import Api, Resource, fields

app = Flask(__name__)
api = Api(app)

task_model = api.model('Task', {
    'name': fields.String(required=True, description='作業名稱', example='洗碗'),
    'completed': fields.Boolean(required=True, description='是否完成', example=False),
})

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):
    def get(self):
        return tasks

    @api.expect(task_model)
    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>')
class TaskDetail(Resource):
    def get(self, task_id):
        return find_task(task_id)

    @api.expect(task_model)
    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

    def delete(self, task_id):
        task = find_task(task_id)
        tasks.remove(task)

        return '', 204

if __name__ == '__main__':
    app.run(debug=True)

PUT 在做什麼?

@api.expect(task_model)
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

流程其實很單純:
先用 task_id 找到那一筆(找不到就 404)
用 api.payload 拿到這次要改成什麼
把值蓋上去
回傳改好的資料

修改和新增一樣需要帶資料進來,所以 put() 上面也要加 @api.expect(task_model),Swagger 才會有輸入框。

這裡有一個小細節:

task[“name”] = data.get(“name”, task[“name”])

意思是「如果這次有傳 name 就用新的,沒有傳就保留原本的」。

順便認識 PUT 和 PATCH

其實嚴格來說,PUT 的語意是「整筆取代」:
使用者要把完整的資料傳進來,沒傳到的欄位就會被清掉。

如果只想改其中一個欄位(例如只把 completed 改成 true),比較正確的 Method 是 PATCH(部分更新)。

我們這次為了單純一點,只使用 PUT,而且寫成「沒傳的欄位就保留原本的值」。

現在先知道有 PATCH 這個東西,以及 PUT 的原本語意是什麼就可以了。

DELETE 在做什麼?

def delete(self, task_id):
    task = find_task(task_id)
    tasks.remove(task)

    return '', 204

一樣先找到那一筆,找不到就 404。

找到之後用 tasks.remove(task) 把它從 List 裡拿掉。

比較特別的是回傳:

return '', 204

204 是 No Content,表示「這次處理成功了,但沒有東西要回傳給你」。

刪除之後資料已經不存在了,也沒什麼好回傳的,所以很常用 204。

如果想讓前端收到一句訊息,也可以寫成:

return {“message”: “刪除成功”}, 200

兩種都有人用,重點是整個 API 要一致。

實際測試

重新啟動:

python app.py

打開

http://127.0.0.1:5000/
https://ithelp.ithome.com.tw/upload/images/20260929/20183865tK9Dqg3obq.png
Swagger 上現在有 GET / POST /tasks,以及 GET / PUT / DELETE /tasks/{task_id}

因為 Server 一重開資料就會不見(資料存在記憶體裡),所以要先用 POST /tasks 新增兩筆來測試。

步驟 1:新增資料

{
  "name": "洗碗",
  "completed": false
}

{
  "name": "寫 Day 15",
  "completed": false
}

步驟 2:用 PUT 把第 1 筆改成完成

點開 PUT /tasks/{task_id}
task_id 填 1
Request Body 改成:

{
  "name": "洗碗",
  "completed": true
}

Execute,Response:

{
  "id": 1,
  "name": "洗碗",
  "completed": true
}

PUT /tasks/1 之後,completed 變成 true
https://ithelp.ithome.com.tw/upload/images/20260929/20183865mHXLs1mdb5.png
再用 GET /tasks 確認一次,第 1 筆的 completed 真的變成 true 了。
https://ithelp.ithome.com.tw/upload/images/20260929/20183865809LS6gMjX.png
修改後用 GET /tasks 確認資料真的被改掉

步驟 3:用 DELETE 刪掉第 2 筆

點開 DELETE /tasks/{task_id}
task_id 填 2
Execute

這次的 Code 會是 204,而且 Response body 是空的。
https://ithelp.ithome.com.tw/upload/images/20260929/201838650HCnBB5cPX.png
DELETE /tasks/2 回傳 204,沒有內容

再用 GET /tasks 看一次,就只剩下第 1 筆了。
https://ithelp.ithome.com.tw/upload/images/20260929/20183865OGI9XrrIQb.png
刪除後 GET /tasks 只剩一筆資料

步驟 4:再刪一次同一筆

剛剛已經把 id 2 刪掉了,如果再 DELETE 一次 /tasks/2:

Code 會是 404,訊息是「找不到 id 為 2 的作業」。
https://ithelp.ithome.com.tw/upload/images/20260929/20183865GD6TzxWRtE.png
重複刪除同一筆,回傳 404

這樣就知道 find_task() 有確實發揮作用。

整理一下四個操作

GET /tasks → 取得全部(200)
POST /tasks → 新增一筆(201)
GET /tasks/{id} → 取得一筆(200 / 404)
PUT /tasks/{id} → 修改一筆(200 / 404)
DELETE /tasks/{id} → 刪除一筆(204 / 404)

到這裡,CRUD 四個操作其實都已經寫完了。

不過現在的程式還有一些可以整理的地方,例如回傳的資料格式沒有統一,Swagger 文件上也看不出每個 API 會回傳什麼。

下一篇就把這些整理起來,完成第一個比較完整的 API。


上一篇
Day 14|CRUD:新增與查詢
系列文
從零開始的後端開發:用 Flask 實作 REST API,搞懂 API 與資料庫之間如何協作 共 15 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言