昨天,這個專案的資料終於搬進了資料庫。但回頭想想,可能會覺得有一件事有點怪怪的:我們從頭到尾,一支 API 都沒寫,網站卻拿到資料了。
其實,AI 幫我們改完程式之後,專案執行時就已經在用 API 了。程式碼裡的 Supabase 套件,背後做的事情,就是帶著網址和公開金鑰,向 Supabase 送出請求、拿回結果。只是這些都被包起來,我們沒看到而已。
今天,我們就把包裝拆開,親手送幾次請求,看看程式之間到底是怎麼說話的。
潛水的時候,船上的人和水下的潛水員之間,常常靠一條繩子溝通。
拉一下,代表「我沒事」;拉三下,代表「拉我上去」。這些意思,要在下水前就約定好。沒有約定,拉繩就只是雜訊:船上的人感覺到繩子在動,卻不知道對方想說什麼。
API 就是程式之間的這份約定: 你要用什麼格式問,我會用什麼格式回答。
API 不一定是網路服務。程式呼叫一個套件公開給你使用的函式,也是在使用那個套件的 API。今天談的,是網站開發最常碰到的 Web API:透過網路,一方送出請求,另一方給出回應。
程式之間的每一次溝通,都是一來一回:一方送出請求,另一方給出回應。
請求(Request) 回應(Response)
┌─────────────────────────┐ ┌─────────────────────────┐
│ 方法:GET │ │ 狀態碼:200 │
│ 網址:/albums │ ──→ │ 內容: │
│ 標頭:apikey: ... │ ←── │ [ │
│ 內容:(GET 通常沒有) │ │ { "title": "..." }, │
└─────────────────────────┘ │ ... │
│ ] │
└─────────────────────────┘
請求裡有四樣東西:
回應裡有兩樣東西:
信號繩真正重要的,不是哪一下對應什麼,而是雙方事先同意同一套規則。Web API 只是把這套規則,寫成了方法、網址、標頭、內容和回應。
不管是什麼產品,對資料能做的事,大致就是四種:新增、讀取、修改、刪除。 取英文字首,就叫 CRUD。
HTTP 替這四件事,各準備了對應的方法:
| CRUD | 意思 | HTTP 方法 | 例子 |
|---|---|---|---|
| Create | 新增 | POST |
新增一筆收藏 |
| Read | 讀取 | GET |
看專輯列表 |
| Update | 修改 | PATCH |
修改一張專輯的資料 |
| Delete | 刪除 | DELETE |
取消一筆收藏 |
修改還有另一個方法叫
PUT。常見的理解是:PATCH修改其中一部分,PUT用一份完整的新內容,取代原本的那一筆。
CRUD 與 HTTP 方法的對應 並非一對一 的規則,上表列的只是常見的對應關係。 ** CRUD 描述的是「對資料做什麼」,HTTP 方法描述的是「這次請求想做什麼」。** 後端收到請求之後,可能只做一件事,也可能做好幾件:例如一次「送出訂單」,背後可能要新增訂單、扣庫存、再寄一封通知信,所以某一種操作對應使用的 HTTP 方法,取決於你如何設計,方法並不唯一。
回應裡的狀態碼,是對方回的第一句話。先看開頭的數字:
最常遇到的幾個:
| 狀態碼 | 白話 |
|---|---|
| 200 | 成功 |
| 201 | 成功,而且新增了一筆資料 |
| 400 | 你送來的請求有問題 |
| 401 | 你還沒證明自己是誰 |
| 403 | 知道你是誰,但你不能做這件事 |
| 404 | 找不到這個東西 |
| 500 | 伺服器出問題了 |
401 和 403 很容易搞混,之後做登入時會再遇到。現在先記住:401 是「你是誰?」,403 是「我知道你是誰,但不行」。
知道了方法和網址,接下來的問題是:網址要怎麼取?
如果每個人都照自己的習慣取名,就會出現這樣的網址:
/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 有一個很方便的地方:它提供一套 Data API,讓我們可以透過網址,存取允許對外開放的資料表。昨天網站拿資料,用的就是它。今天,我們跳過網站,直接去打。
我用的工具是 Postman,一個專門用來送請求的工具:填好方法、網址、標頭,按下送出,就能看到回應,而我使用的是桌面版的。
要準備兩樣東西,都是 Day 15 放進 .env.local 的。注意,不是你網站的網址,而是 Supabase 替你開的那個專案:
Supabase的專案網址/rest/v1/資料表名稱。Supabase 的專案網址就是 .env.local 裡的 NEXT_PUBLIC_SUPABASE_URL,長得像 https://{{你的專案代號}}.supabase.co
apikey,值填公開金鑰

【圖1|新增 Postman 環境變數】

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

【圖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,只是加上了條件。

【圖4| 讀取特定專輯結果】
實驗 3:讀一筆不存在的
GET {{Supabase專案網址}}/rest/v1/albums?select=id,title&id=eq.{{不存在的專輯id}}
直覺上,找不到東西應該是 404。但實際送出,會拿到:
200
[]

【圖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 格式」。

【圖6| 嘗試新增專輯資料】
請求被拒絕了。公開金鑰能做什麼,由資料表的權限決定,而我們昨天只開放了讀取,所以寫不進去。高權限的管理者金鑰(secret key)通常不走一般使用者那套 RLS 限制,因此可以完成這類管理操作。所以管理者金鑰務必保護好,不要洩漏。
修改和刪除就不實際打了,結果會一樣被擋下來。
另外,參數的設定方法有很多種,有興趣的可以自行研究一下。
那如果把網址換成自己的網站呢?(記得要先啟動本地伺服器)
GET http://localhost:3000/albums/aespa-armageddon
一樣是 GET,一樣回 200,但這次拿到的是用來呈現頁面的回應,通常可以看到 HTML,而不是剛才那種專門給程式消費的 JSON 資料。

【圖7| 請求自己的網站】
這一頁是給瀏覽器畫出畫面用的,不是給程式讀資料用的。技術上,你也可以寫程式從這堆 HTML 裡挖出專輯名稱,但這一頁從來沒有承諾過格式,哪天改個版面,你的程式就壞了。
兩個都是 HTTP 請求,差別不在怎麼送,而在回來的東西是給誰用的:頁面的主要對象是瀏覽器與人;Data API 的主要對象是程式,而且它對 request / response 的格式有明確約定。
所以,這個專案現在其實還沒有自己的 API。第六節要設計的 /api/reports,才會是第一支。
如果你看過 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?
瀏覽器
│ GET /albums
▼
專案的頁面(在伺服器上執行)
│ 透過 Supabase 套件
▼
Supabase 的 Data API
│
▼
資料庫
專案的頁面是在伺服器上執行的,它透過 Supabase 套件,直接向 Supabase 的 Data API 拿資料。這條路上的 API,是 Supabase 設計好的,我們只是照著用。
另一種做法,是自己提供一個 API:
瀏覽器、Postman、之後的 App…
│
▼
專案中的 /api/reports
│
自己的檢查規則
│
▼
資料庫
那什麼時候,值得自己再包一層?大概是這幾種情況:
反過來說,不是所有的資料操作,都一定要自己寫 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,又能不能相信?明天,我們就把這扇門真的裝上去,並且想清楚:門口要檢查什麼。
我們明天見。