iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0

上一篇我們讓 API 可以接收資料了:

@api.expect(task_model)
def post(self):
    data = api.payload
    return {"你傳進來的資料": data}, 201

透過 Request Body 把 JSON 傳進來,再用 api.payload 拿到資料。

不過那時候我們只是把收到的資料原封不動回傳回去,並沒有把它存起來。

所以新增完之後,再用 GET /tasks 去查,看到的還是一開始寫死的那一筆:

{
  "name": "Flask",
  "completed": false
}

今天就要讓「新增」和「查詢」真的接起來。

先認識 CRUD

CRUD 是四個操作的縮寫,也是大部分後端會一直重複做的事情:

Create(新增)→ POST
Read(查詢)→ GET
Update(修改)→ PUT
Delete(刪除)→ DELETE

這四個其實就是 Day 5 認識的 HTTP Method,只是換成另一個講法。

今天先做前兩個:Create 和 Read。
Update 和 Delete 留到下一篇。

資料先存在哪裡?

目前我們還沒有學到資料庫,所以先用 Python 的 List 和 Dict 把資料存在程式裡。
每一筆作業是一個 Dict:

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

所有作業放在一個 List 裡:

tasks = [
    {"id": 1, "name": "洗碗", "completed": False},
    {"id": 2, "name": "寫 Day 14", "completed": False},
]

先說清楚一件事:
這種做法只是練習用的,因為資料是存在程式的記憶體裡,只要 Server 一關掉,資料就會全部不見。
這也是 Day 17 會講到「為什麼需要資料庫」的原因,今天先讓流程跑起來。

為什麼每一筆要有 id?

前面幾天我們寫的都是 /tasks,指的是「全部的作業」。

但如果之後要修改或刪除「某一筆」作業,就需要一個方式來指定是哪一筆。

所以每一筆資料通常都會有一個不會重複的編號,這個編號就是 id。

有了 id 之後,就可以寫成:

GET /tasks → 取得全部作業
POST /tasks → 新增一筆作業
GET /tasks/1 → 取得編號 1 的作業

這也是 Day 6 提到的 REST 設計方式:

/tasks → 作業這個 Resource 的集合
/tasks/1 → 集合裡面的其中一筆

開始改程式

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),
})

# 先把資料存在程式裡(Server 關掉就會不見)
tasks = []
next_id = 1

@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):
        for task in tasks:
            if task["id"] == task_id:
                return task

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

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

一段一段看

tasks = [] 和 next_id = 1

tasks 是存放所有作業的 List,一開始是空的。
next_id 是下一筆資料要使用的 id,每新增一筆就加 1,這樣 id 才不會重複。

class TaskList(Resource) 和 class TaskDetail(Resource)

這裡把原本的 Tasks 拆成了兩個類別。

Day 11 有提到,class 的名字跟 Path 沒有直接關係,它只是我們自己取的名字。

真正決定 Path 的是上面的 @api.route()。

/tasks 是「整份清單」,所以叫 TaskList
/tasks/ 是「其中一筆」,所以叫 TaskDetail

這樣之後看程式比較清楚。

def get(self) 直接回傳 tasks

return tasks

這裡直接回傳一個 Python 的 List,Flask-RESTX 會幫我們轉成 JSON 陣列。

所以如果現在還沒有新增任何資料,打開 /tasks 會看到:

[]

post() 裡面的 global next_id

next_id 是寫在函式外面的變數,如果在函式裡面要「改變」它的值,Python 需要我們先寫 global 告訴它:我要改的是外面那個。

(這種寫法只適合現在這種練習,之後資料換成資料庫就不需要了。)

data.get(“completed”, False)

data 是 api.payload 拿到的字典。

data.get(“name”) → 取出 name
data.get(“completed”, False) → 取出 completed,如果沒有傳就當作 False

這裡用 .get() 而不是 data[“completed”],是因為如果使用者沒有傳這個欄位,用中括號會直接讓程式出錯(KeyError),最後變成 500。

(怎麼好好處理「使用者亂傳資料」,Day 24、Day 25 會再講。)

@api.route('/tasks/<int:task_id>')

這是今天另一個重點。
是 Flask 的路徑參數(URL 變數),意思是:
這段路徑是會變動的,把它取出來,轉成整數,然後叫做 task_id
所以:
/tasks/1 → task_id = 1
/tasks/2 → task_id = 2

取出來的值會傳給下面的函式,所以函式要寫成:

def get(self, task_id):

順帶一提,int:… 裡的 int 也有檢查的效果。
如果打開 /tasks/abc,因為 abc 不是整數,Flask 會直接回 404,不會進到我們的程式裡。

api.abort(404, ...)

如果整個 List 找過一遍都沒有這個 id,就代表這筆資料不存在。

這時候不應該回傳空白或 200,而是要告訴前端「找不到」。

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

api.abort() 會立刻中斷這次的處理,並回傳我們指定的狀態碼。

Day 24 會再完整介紹狀態碼和錯誤處理。

實際測試看看

重新啟動:

python app.py

打開

http://127.0.0.1:5000/
https://ithelp.ithome.com.tw/upload/images/20260928/20183865F8bjvhHYmo.png
Swagger 上現在有 GET /tasks、POST /tasks、GET /tasks/{task_id} 三個 API

步驟 1:先用 GET /tasks 看看
Try it out → Execute
因為還沒有新增任何資料,Response 會是:

[]

GET /tasks 回傳空陣列 []
https://ithelp.ithome.com.tw/upload/images/20260928/20183865i7pMpF7Vym.png
步驟 2:用 POST /tasks 新增一筆
Try it out,把 Request Body 改成:

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

Execute 之後,Response 會是:

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

可以看到 API 幫我們補上了 id,狀態碼是 201。
https://ithelp.ithome.com.tw/upload/images/20260928/201838655Y24CcH5bm.png
POST /tasks 新增成功,Response 是 201 和帶有 id 的資料
再新增一筆,例如:

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

這次的 id 會是 2。
步驟 3:再用 GET /tasks 查一次
這次就會看到兩筆資料:

[
  {
    "id": 1,
    "name": "洗碗",
    "completed": false
  },
  {
    "id": 2,
    "name": "寫 Day 14",
    "completed": false
  }
]

GET /tasks 這次回傳兩筆資料
https://ithelp.ithome.com.tw/upload/images/20260928/20183865FroHQy1R4U.png
新增和查詢終於接起來了。
步驟 4:用 GET /tasks/{task_id} 查其中一筆

在 task_id 欄位填 1,Execute:

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

GET /tasks/1 只回傳一筆資料
https://ithelp.ithome.com.tw/upload/images/20260928/201838652RyjC9jCk5.png
步驟 5:故意查一個不存在的
在 task_id 填 999,Execute:
Code 會變成 404,Response 大概是:

{
  "message": "找不到 id 為 999 的作業"
}

GET /tasks/999 回傳 404
https://ithelp.ithome.com.tw/upload/images/20260928/20183865LI2XRaOfmC.png
整個流程:

POST /tasks(帶著 JSON)
        ↓
執行 post(),api.payload 拿到資料
        ↓
補上 id,append 進 tasks
        ↓
回傳新增好的那一筆(201)
GET /tasks
        ↓
執行 get(),回傳整個 tasks
        ↓
Flask-RESTX 轉成 JSON 陣列

今天完成了 CRUD 的前兩個:

POST /tasks → 新增一筆,並自動給一個 id
GET /tasks → 取得全部
GET /tasks/{id} → 取得其中一筆,找不到就回 404

也順便認識了路徑參數 。

不過現在新增進去的資料,還是只能看、不能改。

如果今天作業寫完了,想把 completed 改成 true,或想把打錯的作業刪掉,要怎麼做?

下一篇就來完成 CRUD 的另外兩個:修改(PUT)和刪除(DELETE)。


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

尚未有邦友留言

立即登入留言