iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
Vibe Coding

《我與 AI 的奇幻漂流:30 天,把「能跑」變成「能上線」》系列 第 16 篇

【Day 16|信號繩】CRUD 與 RESTful API:程式之間怎麼約定彼此說什麼

  • 分享至 

  • xImage
  •  

昨天,這個專案的資料終於搬進了資料庫。但回頭想想,可能會覺得有一件事有點怪怪的:我們從頭到尾,一支 API 都沒寫,網站卻拿到資料了。

其實,AI 幫我們改完程式之後,專案執行時就已經在用 API 了。程式碼裡的 Supabase 套件,背後做的事情,就是帶著網址和公開金鑰,向 Supabase 送出請求、拿回結果。只是這些都被包起來,我們沒看到而已。

今天,我們就把包裝拆開,親手送幾次請求,看看程式之間到底是怎麼說話的。


潛水的時候,船上的人和水下的潛水員之間,常常靠一條繩子溝通。

拉一下,代表「我沒事」;拉三下,代表「拉我上去」。這些意思,要在下水前就約定好。沒有約定,拉繩就只是雜訊:船上的人感覺到繩子在動,卻不知道對方想說什麼。

API 就是程式之間的這份約定: 你要用什麼格式問,我會用什麼格式回答。

API 不一定是網路服務。程式呼叫一個套件公開給你使用的函式,也是在使用那個套件的 API。今天談的,是網站開發最常碰到的 Web API:透過網路,一方送出請求,另一方給出回應。


一、一次請求,一次回應

程式之間的每一次溝通,都是一來一回:一方送出請求,另一方給出回應。

請求(Request)                    回應(Response)
┌─────────────────────────┐       ┌─────────────────────────┐
│ 方法:GET                │       │ 狀態碼:200               │
│ 網址:/albums            │  ──→  │ 內容:                   │
│ 標頭:apikey: ...        │  ←──  │ [                       │
│ 內容:(GET 通常沒有)     │       │   { "title": "..." },   │
└─────────────────────────┘       │   ...                   │
                                  │ ]                       │
                                  └─────────────────────────┘

請求裡有四樣東西:

  • 方法:這次想做什麼,例如「讀取」還是「新增」
  • 網址:要找的是哪個東西
  • 標頭:附帶的資訊,例如「我是誰」「我送的是什麼格式」
  • 內容:要送過去的資料,例如新增時要寫入的內容

回應裡有兩樣東西:

  • 狀態碼:一個三位數的數字,說明這次請求的結果
  • 內容:回傳的資料,通常是 JSON 格式

信號繩真正重要的,不是哪一下對應什麼,而是雙方事先同意同一套規則。Web API 只是把這套規則,寫成了方法、網址、標頭、內容和回應。


二、能說哪些話:CRUD 和狀態碼

2.1 CRUD:對資料能做的四件事

不管是什麼產品,對資料能做的事,大致就是四種:新增、讀取、修改、刪除。 取英文字首,就叫 CRUD。

HTTP 替這四件事,各準備了對應的方法:

CRUD 意思 HTTP 方法 例子
Create 新增 POST 新增一筆收藏
Read 讀取 GET 看專輯列表
Update 修改 PATCH 修改一張專輯的資料
Delete 刪除 DELETE 取消一筆收藏

修改還有另一個方法叫 PUT。常見的理解是:PATCH 修改其中一部分,PUT 用一份完整的新內容,取代原本的那一筆。

CRUD 與 HTTP 方法的對應 並非一對一 的規則,上表列的只是常見的對應關係。 ** CRUD 描述的是「對資料做什麼」,HTTP 方法描述的是「這次請求想做什麼」。** 後端收到請求之後,可能只做一件事,也可能做好幾件:例如一次「送出訂單」,背後可能要新增訂單、扣庫存、再寄一封通知信,所以某一種操作對應使用的 HTTP 方法,取決於你如何設計,方法並不唯一。

2.2 狀態碼:回應的第一句話

回應裡的狀態碼,是對方回的第一句話。先看開頭的數字:

  • 2 開頭:這次請求成功處理了
  • 4 開頭:伺服器認為這次請求不能完成,通常跟請求的內容、身分、權限,或指定的東西有關
  • 5 開頭:伺服器自己出了問題

最常遇到的幾個:

狀態碼 白話
200 成功
201 成功,而且新增了一筆資料
400 你送來的請求有問題
401 你還沒證明自己是誰
403 知道你是誰,但你不能做這件事
404 找不到這個東西
500 伺服器出問題了

401 和 403 很容易搞混,之後做登入時會再遇到。現在先記住:401 是「你是誰?」,403 是「我知道你是誰,但不行」。


三、怎麼說得一致:RESTful

知道了方法和網址,接下來的問題是:網址要怎麼取?

如果每個人都照自己的習慣取名,就會出現這樣的網址:

/getAlbums
/createReport
/deleteFavorite?id=123

能用,但每一支都得另外記。

RESTful 是一種常見的設計風格,核心概念很簡單:

網址放「東西」,方法放「動作」。

各取各的 RESTful 的寫法
/getAlbums GET /albums
/getAlbum?id=armageddon GET /albums/armageddon
/createReport POST /reports
/deleteFavorite?id=123 DELETE /favorites/123

網址只描述「東西在哪裡」,要對它做什麼,交給方法決定。這樣一來,看到 /albums,就大概猜得到 GET 是讀取、POST 是新增。

篩選條件,則放在網址的 ? 後面。還記得 Day 11 的 /albums?q=aespa&type=mini 嗎?那就是同一個概念:東西還是 albums,只是加上了條件。

RESTful 是約定俗成的風格,不是硬性規定。也不是網址長這樣,就自動變成 RESTful。它的價值在於:大家照同一套習慣取名,彼此比較好溝通。


四、動手:打 Supabase 的 API

說了這麼多,來真的打一次。

Supabase 有一個很方便的地方:它提供一套 Data API,讓我們可以透過網址,存取允許對外開放的資料表。昨天網站拿資料,用的就是它。今天,我們跳過網站,直接去打。

4.1 準備

我用的工具是 Postman,一個專門用來送請求的工具:填好方法、網址、標頭,按下送出,就能看到回應,而我使用的是桌面版的。

要準備兩樣東西,都是 Day 15 放進 .env.local 的。注意,不是你網站的網址,而是 Supabase 替你開的那個專案:

  • 網址:Supabase的專案網址/rest/v1/資料表名稱。Supabase 的專案網址就是 .env.local 裡的 NEXT_PUBLIC_SUPABASE_URL,長得像 https://{{你的專案代號}}.supabase.co
  • 標頭:名稱填 apikey,值填公開金鑰

https://ithelp.ithome.com.tw/upload/images/20260930/20178017AH5jVcmrjf.png
【圖1|新增 Postman 環境變數】

https://ithelp.ithome.com.tw/upload/images/20260930/201780175KYHtvmQib.png
【圖2| Postman 環境變數設定】

只用公開金鑰。 Postman 的工作區可能會同步到雲端,管理者金鑰絕對不要貼進去。Day 15 說過,「別人」也包括你用的工具。
可以直接在介面上填完整個網址,也可以把網址、公開金鑰存成環境變數讓Postman自己去讀取:添加方式是左側面板的「+」→ 「新增environment」→ 添加參數名稱、值,後續再請求的時候,在右上角記得切換 environment

4.2 四個實驗

實驗 1:讀全部

GET {{Supabase專案網址}}/rest/v1/albums?select=id,title

這個網址是 Supabase 官方 Data API 的格式,拆開來看是這樣:

片段 意思
https://你的專案代號.supabase.co 你的 Supabase 專案
/rest Supabase 裡負責「用網址存取資料表」的服務
/v1 這個服務的第一版。版本號放進網址,之後規則大改可以推出 /v2,舊的程式不會突然壞掉
/albums 要存取的資料表

select=id,title 的意思是「只要 id 和 title 這兩個欄位」。送出之後,應該會看到狀態碼 200,以及 25 張專輯的清單。

https://ithelp.ithome.com.tw/upload/images/20260930/20178017Avno9m7s4L.png
【圖3| 讀取專輯列表結果】

這樣不就誰都能把資料抓走?

對。只要一張表開放公開讀取,知道網址和公開金鑰,就能把整張表抓下來。而公開金鑰本來就放在前端,表名也要當成別人查得到。

以專輯目錄來說,這是刻意的:它本來就是公開資料,網站上人人都看得到。

但換成收藏就不一樣了。如果收藏的資料表也開放公開讀取,任何人都能用同樣的方式,看到每個人收藏了哪些專輯。真正保護資料的,不是藏好金鑰或表名,而是權限規則。 所以新增一張表時,要先問自己:「如果任何人都能讀到這張表,我可以接受嗎?」不能接受的,就不開放公開讀取。
所以在開發過程中,每一個 Table 都要確認誰可以讀。 另外,有時候也會限制 rate limit 來避免有心人士大量的呼叫 API 把資料爬走。

實驗 2:讀一筆

GET {{Supabase專案網址}}/rest/v1/albums?select=id,title&id=eq.armageddon

id=eq.armageddon 是 Supabase 的篩選寫法,意思是「id 等於 armageddon」。這次只會拿回一筆。

你可能會想:第三節不是說 RESTful 的寫法是 /albums/aespa-armageddon 嗎?RESTful 是一種風格,不代表每個 API 的網址都長得一模一樣。Supabase 選擇把條件都放在 ? 後面,這是它自己的設計;東西還是 albums,只是加上了條件。

https://ithelp.ithome.com.tw/upload/images/20260930/201780174SDuVzS40T.png
【圖4| 讀取特定專輯結果】

實驗 3:讀一筆不存在的

GET {{Supabase專案網址}}/rest/v1/albums?select=id,title&id=eq.{{不存在的專輯id}}

直覺上,找不到東西應該是 404。但實際送出,會拿到:

200
[]

https://ithelp.ithome.com.tw/upload/images/20260930/20178017rrtPwiv8Qk.png
【圖5| 讀取不存在的專輯】

為什麼?因為這個 API 的意思是「在專輯裡,找出符合條件的」。搜尋這件事本身成功了,只是結果是 0 筆。

這說明了一件很重要的事:狀態碼要怎麼回,是設計 API 時的一個決定。 當然不是想回什麼就回什麼,每個狀態碼都有既定的意思;但哪一個意思符合這個 API 的情境,要看這個 API 怎麼定義自己。

實驗 4:用公開金鑰新增一筆

最後,來試試 Day 15 AI 替資料表設定的唯讀權限。

POST {{Supabase專案網址}}/rest/v1/albums
標頭:apikey(公開金鑰)、Content-Type: application/json
內容:{ "id": "test-album", "title": "Test" }

Content-Type: application/json 是告訴對方:「我送的內容是 JSON 格式」。

https://ithelp.ithome.com.tw/upload/images/20260930/20178017FzSQVqYe1p.png
【圖6| 嘗試新增專輯資料】

請求被拒絕了。公開金鑰能做什麼,由資料表的權限決定,而我們昨天只開放了讀取,所以寫不進去。高權限的管理者金鑰(secret key)通常不走一般使用者那套 RLS 限制,因此可以完成這類管理操作。所以管理者金鑰務必保護好,不要洩漏。

修改和刪除就不實際打了,結果會一樣被擋下來。
另外,參數的設定方法有很多種,有興趣的可以自行研究一下。

4.3 打自己的網站看看

那如果把網址換成自己的網站呢?(記得要先啟動本地伺服器)

GET http://localhost:3000/albums/aespa-armageddon

一樣是 GET,一樣回 200,但這次拿到的是用來呈現頁面的回應,通常可以看到 HTML,而不是剛才那種專門給程式消費的 JSON 資料。

https://ithelp.ithome.com.tw/upload/images/20260930/20178017SOJAVodeU3.png
【圖7| 請求自己的網站】

這一頁是給瀏覽器畫出畫面用的,不是給程式讀資料用的。技術上,你也可以寫程式從這堆 HTML 裡挖出專輯名稱,但這一頁從來沒有承諾過格式,哪天改個版面,你的程式就壞了。

兩個都是 HTTP 請求,差別不在怎麼送,而在回來的東西是給誰用的:頁面的主要對象是瀏覽器與人;Data API 的主要對象是程式,而且它對 request / response 的格式有明確約定。

所以,這個專案現在其實還沒有自己的 API。第六節要設計的 /api/reports,才會是第一支。

4.4 AI 為什麼一直在用 curl?

如果你看過 AI 的操作紀錄,可能常看到很多 curl 開頭的指令,例如:

curl "Supabase專案網址/rest/v1/albums?select=id,title" \
  -H "apikey: 公開金鑰"

這其實跟實驗 1 是同一個請求,只是寫法不同:

  • curl:我要送出一個請求
  • 引號裡的網址:送去哪裡
  • H:附帶的標頭

如果是新增,還會看到 -X POST(指定方法)和 -d(要送的內容)。

Postman 是用畫面組請求,curl 是用文字組請求,做的是同一件事。 AI 在終端機裡工作時,很常用 curl。你不一定要自己背,但看到 AI 在打什麼,要看得懂。(這也是今天分享這篇的目的之一,現在 AI 那麼發達的情況,其實已經比較少人工打 API 了。但親手打過,會更了解 API、HTTP 請求的原理,之後有錯也比較好排查。)


五、用別人的 API,和開一扇自己的門

5.1 用別人的 API

回到開頭的問題:昨天為什麼不用自己寫 API?

瀏覽器
   │  GET /albums
   ▼
專案的頁面(在伺服器上執行)
   │  透過 Supabase 套件
   ▼
Supabase 的 Data API
   │
   ▼
資料庫

專案的頁面是在伺服器上執行的,它透過 Supabase 套件,直接向 Supabase 的 Data API 拿資料。這條路上的 API,是 Supabase 設計好的,我們只是照著用。

5.2 開一扇自己的門

另一種做法,是自己提供一個 API:

瀏覽器、Postman、之後的 App…
                  │
                  ▼
     專案中的 /api/reports
                  │
          自己的檢查規則
                  │
                  ▼
               資料庫

那什麼時候,值得自己再包一層?大概是這幾種情況:

  • 很多地方都要用同一份約定,例如如果要同時開發網站和手機 App
  • 請求進來之後,要先跑自己的規則,例如檢查格式、確認資料合理
  • 伺服器要用到不能公開的金鑰
  • 要同時串接其他服務,例如收到之後順便通知自己

反過來說,不是所有的資料操作,都一定要自己寫 API。 像昨天的讀取,直接用 Supabase 的就很好;之後有了登入,有些寫入也可以靠權限規則,安全地直接交給 Supabase。


換個領域:用同一套約定設計網址

記帳

要做的事 請求範例
看這個月的交易 GET /transactions?month=2026-10
新增一筆交易 POST /transactions
修改一筆交易的金額 PATCH /transactions/123
刪除一筆交易 DELETE /transactions/123

點餐

要做的事 請求範例
看菜單 GET /menu-items
送出訂單 POST /orders
查訂單狀態 GET /orders/456

不管是什麼產品,都是同一套思路:先想清楚「東西」是什麼,再決定要對它做什麼。


結語與明日預告

今天沒有改任何一行專案的程式,但我們親手做了一件事:跳過專案的前端畫面,直接跟 Supabase 的 Data API 說話。 這也證明了一件事:畫面不是唯一的入口。

不需要點你的按鈕,也不需要填你的表單,只要知道網址和規則,任何人都能直接送出請求。


接下來,我想要在專案中實作第一個讓使用者可以「寫入」的功能:回報資料錯誤。

我目前的打算是,回報的入口會放在專輯詳情裡:使用者發現內容物不對,填一句說明就能送出,專輯和版本的 id 由頁面自動帶入,不用自己填。這個功能要先檢查送來的內容,之後還要通知我,所以我打算開一扇自己的門,準備開始著手第一支我開發的API。

但今天已經知道,畫面不是唯一的入口。如果只在輸入框上限制字數,真的擋得住嗎?頁面自動帶入的 id,又能不能相信?明天,我們就把這扇門真的裝上去,並且想清楚:門口要檢查什麼。

我們明天見。


上一篇
【Day 15|海面之下】資料庫與 Schema:從 data.ts 到真正的資料表
下一篇
【Day 17|船舷之外】API 實作與請求驗證:送進來的資料,真的能信嗎?
系列文
《我與 AI 的奇幻漂流:30 天,把「能跑」變成「能上線」》 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言