PostgREST 自動 API 入門 | Supabase 完整教學
PostgREST 是 Supabase 自動 API 的心臟——你在資料庫裡建好一張 table,它就自動幫你生出一整套 REST API,端點長得像
/rest/v1/products,支援讀寫、篩選、排序、分頁,完全不用手寫後端。這一篇會帶你認識這套「建表即有 API」的魔法:endpoint 怎麼組成、apikey與Authorization: Bearer兩個 header 各自負責什麼、為什麼 REST 一樣受 RLS 保護,並對照curl直呼與supabase-js兩種寫法,最後解決「新建的表 API 卻看不到」的 schema cache 問題。
前言
一句話定義本篇主題:PostgREST 是一個「架在 PostgreSQL 之上的自動 API 層」,它讀你的資料庫結構,把每一張表都變成一個現成的 REST 端點——你不用寫任何一行後端程式碼,設計好資料表就等於設計好了 API。
如果你來自傳統的後端開發,過去要讓前端能存取一張 products 表,你得:寫一個 ProductsController、定義 GET /products、POST /products 等路由、在每個路由裡手動組 SQL 或呼叫 ORM、處理分頁參數、處理錯誤格式……光是一張表就一堆樣板程式碼。PostgREST(讀作 post-rest) 把這整段全部自動化了:它是一個獨立的服務,開機時直接去讀 PostgreSQL 的系統目錄(System Catalog,資料庫用來描述自己有哪些表、欄位、函式的內部表),然後就地把每張 table、view、function 映射成對應的 REST 端點。
用一個類比:PostgREST 就像一位「隨叫隨到的 API 翻譯官」。你(開發者)只跟資料庫用 SQL 溝通、專心把資料表設計好;前端用它熟悉的 HTTP 語言來提問。這位翻譯官站在中間,把前端的 GET /rest/v1/products?price=lt.100 即時翻成資料庫聽得懂的 SELECT * FROM products WHERE price < 100,再把結果翻回 JSON 送出去。你不用教它任何一句話——它自己看資料表就知道該怎麼翻。而且它翻譯得極快,因為每個 HTTP 請求最終都被解析成單一 SQL 語句。
從這一篇開始,我們正式從系列的第一大主題「資料庫核心(SB-1)」跨進第二大主題——存取層(SB-2)。前面幾篇我們把地基(資料表、關聯、RLS)打穩了,現在要來認識蓋在地基上的「門面」:資料是怎麼透過 API 被安全地送進送出的。
本篇你會學到:
- 建表即有 API:PostgREST 如何從 schema 自動生成 REST 端點,以及
/rest/v1/<table>的結構 - 兩個關鍵 header:
apikey(哪個應用)與Authorization: Bearer(哪個使用者)各自的職責 - REST 與 RLS 的關係:為什麼透過 REST 存取一樣受 RLS 保護、anon key 為何能安全放前端
- 兩種呼叫方式:
curl直呼 REST 端點 vssupabase-js,以及schema cache重載的處理
核心概念
要理解 Supabase 的自動 API,先在腦中建立這條資料流水線:
前端 / 客戶端(瀏覽器、App、curl)
│ HTTP 請求:GET /rest/v1/products?select=id,name
▼
Supabase API Gateway
│ 依路徑分流:/rest/v1/ → PostgREST
▼
PostgREST ──讀取──▶ PostgreSQL 系統目錄(知道有哪些表)
│ 把 HTTP 請求翻成「單一 SQL 語句」
▼
PostgreSQL ──套用 RLS──▶ 只回傳這個角色被允許看到的列
│
▼ JSON 回應
前端 / 客戶端
這條線的每一段都值得拆開看。
建表即有 API:PostgREST 如何從 schema 生成端點
PostgREST 是「無狀態」的——它不儲存任何 API 定義,一切都即時從資料庫結構推導。它會把 public schema(預設暴露的 schema)中每一種資源,映射成一個 REST 端點:
| 資源類型 | 端點格式 | 範例 |
|---|---|---|
| 資料表(Table) | /rest/v1/<table_name> | /rest/v1/products |
| 視圖(View) | /rest/v1/<view_name> | /rest/v1/active_users |
| RPC 函式(Function) | /rest/v1/rpc/<function_name> | /rest/v1/rpc/get_stats |
所以當你在 SQL Editor 執行 create table products (...),你其實同時做了兩件事:建立資料表**,以及**發布 /rest/v1/products 這個 API 端點。它立刻支援:
GET— 讀取(含篩選、排序、分頁)POST— 新增PATCH— 更新DELETE— 刪除
這就是「你的 schema 就是你的 API」這句話的實際意義。
端點結構:基底 URL 與 /rest/v1/
每個 Supabase 專案都有一個固定的 API 基底 URL,格式是:
https://<project_ref>.supabase.co/rest/v1/
<project_ref> 是你專案的唯一代號(在 Dashboard 的 Project Settings → API 可以找到)。所有 REST 請求都掛在 /rest/v1/ 這個前綴之下——/rest 代表這是 REST API(相對於 /graphql/v1、/auth/v1 等其他服務),/v1 是版本號。把前綴接上表名,就是完整端點:
https://abcdefgh.supabase.co/rest/v1/products
兩個關鍵 header:apikey 與 Authorization
這是初學者最常卡住的地方。每一個 REST 請求,通常要帶兩個 header,它們回答兩個不同的問題:
| Header | 回答的問題 | 帶什麼值 |
|---|---|---|
apikey | 「是哪個應用在存取?」 | 專案的 anon(publishable)金鑰 |
Authorization: Bearer <token> | 「是哪個使用者在存取?」 | 使用者登入後的 JWT(access token) |
apikey是「入場券」——沒帶它,PostgREST 直接回401,連理都不理你。它帶的是專案層級的公開金鑰。Authorization: Bearer <jwt>是「身分證」——它讓 PostgREST 知道現在是哪個登入使用者,並把身分資訊注入資料庫 session,讓 RLS 政策裡的(select auth.uid())拿得到值。
一個常見的困惑是:匿名存取時,這兩個 header 要放什麼? 答案是——兩個都放同一把 anon key。因為 anon key 本身就是一個「代表匿名者」的 JWT,把它同時當入場券和身分證用,PostgREST 就會以 anon 角色執行請求。等使用者登入後,apikey 保持 anon key 不變、只把 Authorization 換成該使用者的 access_token,請求就升級成 authenticated 角色。
REST 與 RLS 的關係:API 一樣受 RLS 保護
這是整篇最重要的觀念,請記牢:透過 REST API 存取,不會繞過你設定的 RLS——恰恰相反,REST 請求和 supabase-js 走的是同一條路(都是 PostgREST),所以你在資料庫層設好的 RLS 政策,對 REST 一樣生效。
背後的機制是這樣的:anon 與 authenticated 金鑰對應的都是 PostgreSQL 裡的低權限角色。PostgREST 拿著這個角色連進資料庫,PostgreSQL 就會對它逐條套用 RLS 政策——過不了政策的列,根本不會出現在結果裡;連整張表都不被允許存取時,直接回 401/403。
這也解答了另一個新手常有的恐懼:「anon key 會被前端 F12 直接看到,這樣不會很危險嗎?」 不會。因為 anon key 本身沒有任何特權,它只是一張「以最低權限角色進門」的票;真正決定你能拿到什麼資料的,是 RLS。把 anon key 想成「大樓的公共大門門禁卡」——人人都能拿,但每個房間(資料列)還有各自的鎖(RLS 政策),沒鑰匙照樣進不去。
唯一的例外是 service_role(私密)金鑰:它對應的角色有 BYPASSRLS 屬性,會完全繞過所有 RLS。正因如此,它絕對只能放在後端伺服器,永遠不能出現在前端程式碼或 git 版本控制裡。
關鍵術語速查
| 術語 | 意義 |
|---|---|
| PostgREST | 架在 PostgreSQL 上、自動把 schema 變成 REST API 的服務 |
| System Catalog(系統目錄) | 資料庫描述自己結構的內部表,PostgREST 靠讀它生成 API |
apikey header | 識別「哪個應用」,帶專案的 anon/service_role 金鑰 |
Authorization: Bearer | 識別「哪個使用者」,帶登入後的 JWT |
| anon key(publishable) | 公開金鑰,低權限、受 RLS 限制,可放前端 |
| service_role key | 私密金鑰,BYPASSRLS、繞過 RLS,只能放後端 |
| schema cache(結構快取) | PostgREST 快取的 schema 結構,改結構後需重載 |
實作範例
以下範例假設你已經有一張啟用了 RLS 的 products 表。我們會把「同一件事」用 curl 直呼 REST 端點和 supabase-js 兩種方式並排示範,讓你看清楚 supabase-js 底層其實就是在幫你組這些 HTTP 請求。
先設兩個環境變數方便閱讀(實際值請換成你自己專案的):
# 你的專案基底 URL 與 anon 金鑰(Dashboard → Project Settings → API)
export SUPABASE_URL="https://<project_ref>.supabase.co"
export ANON_KEY="<你的 anon key>"
範例一:curl 直呼 REST 端點讀取資料
最基本的 GET——注意兩個 header 都帶了 anon key(一個當 apikey、一個當 Authorization):
# 讀取 products 表全部欄位(匿名角色,受 RLS 限制)
curl "$SUPABASE_URL/rest/v1/products" \
-H "apikey: $ANON_KEY" \
-H "Authorization: Bearer $ANON_KEY"
# 只取需要的欄位(用 select query 參數做垂直過濾)
curl "$SUPABASE_URL/rest/v1/products?select=id,name,price" \
-H "apikey: $ANON_KEY" \
-H "Authorization: Bearer $ANON_KEY"
# 帶條件:只要價格小於 100 的(column=operator.value 格式)
curl "$SUPABASE_URL/rest/v1/products?price=lt.100&order=price.asc" \
-H "apikey: $ANON_KEY" \
-H "Authorization: Bearer $ANON_KEY"
回應是一個 JSON 陣列,例如 [{"id":1,"name":"Widget","price":29.99}, ...]。這裡的重點是:你沒有寫任何後端——products 這個端點、select/price=lt. 這些查詢語法,全都是 PostgREST 自動提供的。
範例二:curl 新增一筆資料
新增用 POST,多帶 Content-Type: application/json;若想讓 API 把新增的資料回傳給你,再加一個 Prefer: return=representation:
# 新增一筆 product(return=representation 讓回應帶回新增的完整資料)
curl -X POST "$SUPABASE_URL/rest/v1/products" \
-H "apikey: $ANON_KEY" \
-H "Authorization: Bearer $ANON_KEY" \
-H "Content-Type: application/json" \
-H "Prefer: return=representation" \
-d '{"name": "New Widget", "price": 19.99}'
如果你的 RLS 政策不允許 anon 角色 insert,這裡就會收到 401/403——這正是 RLS 在 REST 上照常生效的證明。
範例三:用 supabase-js 做同樣的事
同樣三件事(讀全部、選欄位加篩選、新增),換成 supabase-js。你會發現你只在建立 client 時傳一次 anon key,之後那兩個 header 都由 SDK 自動幫你補上:
import { createClient } from '@supabase/supabase-js'
// 建立 client:這裡傳的 anon key,SDK 之後會自動放進每個請求的 apikey 與 Authorization
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
)
// 對應範例一:讀取全部
const { data, error } = await supabase
.from('products')
.select()
// 對應範例一:選欄位 + 篩選 + 排序
const { data: cheap } = await supabase
.from('products')
.select('id, name, price')
.lt('price', 100)
.order('price', { ascending: true })
// 對應範例二:新增(.select() 等同 Prefer: return=representation)
const { data: inserted } = await supabase
.from('products')
.insert({ name: 'New Widget', price: 19.99 })
.select()
把兩種寫法對照著看,你會發現它們是一模一樣的東西:.from('products') 就是 /rest/v1/products、.lt('price', 100) 就是 ?price=lt.100、.select() 就是 Prefer: return=representation。supabase-js 只是一個更好寫、有型別提示的「HTTP 請求產生器」。也因此,理解底層的 REST 語法,能讓你在 debug(例如看瀏覽器的 Network 分頁)時一眼看懂 SDK 到底發了什麼請求。
範例四:使用者登入後,換上使用者的 JWT
當使用者登入後,你要以「這位使用者」的身分存取(好讓 RLS 的 auth.uid() 生效)。用 curl 時,apikey 保持 anon key、把 Authorization 換成使用者的 access token:
# apikey 仍是 anon key,但 Authorization 換成「這位使用者」登入後拿到的 JWT
curl "$SUPABASE_URL/rest/v1/orders" \
-H "apikey: $ANON_KEY" \
-H "Authorization: Bearer <使用者的 access_token>"
用 supabase-js 時你不用手動做這件事——只要使用者透過 supabase.auth 登入過,SDK 就會自動把該使用者的 access_token 附加到後續每一個請求的 Authorization header:
// 使用者登入後,SDK 自動用其 JWT 發後續請求,這裡的查詢就以 authenticated 角色執行
const { data: myOrders } = await supabase
.from('orders')
.select('*')
// 若 orders 表有「只能看自己訂單」的 RLS,這裡自動只回傳這位使用者的訂單
範例五:schema cache reload —— 新表看不到怎麼辦
PostgREST 為了效能,會把資料庫結構快取在記憶體裡(schema cache)。絕大多數情況下,你在 Dashboard 改結構後,快取會在數秒內自動重載,API 立刻反映新表、新欄位。但偶爾(例如透過外部工具、migration 腳本改結構後)API 會回 404 Not Found 或看不到新欄位——這通常就是快取還沒更新。
手動觸發重載的標準做法,是對 PostgreSQL 送一個 NOTIFY 訊號,PostgREST 收到就會重新讀取 schema:
-- 在 SQL Editor 執行:通知 PostgREST 重新載入 schema cache
notify pgrst, 'reload schema';
如果連權限(GRANT/角色)也改了,可以連同設定一起重載:
-- 同時重載 schema 結構與角色權限設定
notify pgrst, 'reload schema';
notify pgrst, 'reload config';
你也可以在 Supabase Dashboard 的 API 設定頁點「Reload Schema」按鈕達到同樣效果。記住這個訊號,能讓你在「新建的表 API 卻 404」時不再抓瞎。
常見錯誤與最佳實踐
坑一:沒帶 apikey,請求直接 401。
這是新手第一個踩的坑。只帶了 Authorization、忘了 apikey,或以為瀏覽器裡試 URL 就能拿到資料——PostgREST 收到沒有 apikey 的請求,一律回 401 Unauthorized,連查詢都不會執行。正確做法:每個 REST 請求都要帶 apikey header。匿名存取時,apikey 與 Authorization 兩個 header 都放 anon key。(用 supabase-js 則完全不用煩惱,SDK 自動補。)
坑二:以為「走 REST 就能繞過 RLS」。 有些人誤以為 RLS 只擋 supabase-js,改用 curl 直打 REST 端點就能拿到全部資料——這是徹底的誤解。REST 與 supabase-js 底層是同一個 PostgREST、同一組角色,RLS 對兩者一視同仁。帶 anon key 打 REST,一樣被 RLS 逐列過濾。正確做法:把 RLS 當成所有存取路徑(REST、supabase-js、GraphQL)共用的最後防線,別依賴「前端不呼叫某個端點」來保護資料。真正該防的是 service_role 金鑰外洩——那才是唯一的後門。
坑三:把 service_role 金鑰放進前端。
anon key 放前端是安全的(它受 RLS 保護),但 service_role key 絕對不行——它有 BYPASSRLS,一旦洩漏,任何人都能讀寫你整個資料庫。正確做法:service_role key 只放後端環境變數(如 SUPABASE_SERVICE_ROLE_KEY),前端一律只用 anon/publishable key;並確認 .env 有進 .gitignore,別不小心 commit 進 git。
// ✅ 前端:只用 anon key,受 RLS 保護
const supabase = createClient(URL, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY)
// ✅ 後端(且僅後端):service_role key 繞過 RLS,做管理操作
const supabaseAdmin = createClient(URL, process.env.SUPABASE_SERVICE_ROLE_KEY)
坑四:改了結構,API 卻看不到新表/新欄位。
建好新表或加了欄位,API 卻回 404 或查不到新欄位——多半是 schema cache 還沒重載。正確做法:大多數情況等幾秒會自動好;若沒有,在 SQL Editor 執行 notify pgrst, 'reload schema';,或到 Dashboard 點「Reload Schema」。如果連權限也調了,補一句 notify pgrst, 'reload config';。
坑五:新表建好卻忘了開 RLS,資料全裸奔。
在 Supabase,一張表只要建在 public schema,它的 REST 端點就自動公開了。如果你忘了 enable row level security、又給了 anon 角色權限,那這張表的資料等於對全世界公開可讀寫。正確做法:每建一張對外的表,養成「建表 → 立刻 alter table ... enable row level security → 寫政策」的固定流程;Dashboard 的 Table Editor 也會在未開 RLS 時對你發出警告,別忽略它。
最佳實踐總結:
- 每個 REST 請求都帶
apikey:匿名時apikey與Authorization都放 anon key - RLS 是共用防線:REST、supabase-js、GraphQL 一律受 RLS 保護,別以為 REST 有後門
- 金鑰分工要清楚:anon key 放前端、service_role key 只放後端且不進 git
- 改結構記得 reload:新表 404 時用
notify pgrst, 'reload schema';手動重載 - 建表即開 RLS:
publicschema 的表自動公開,務必把「開 RLS」納入建表 SOP
小結
這是 Supabase 系列教學的第 012 篇,也是我們的第二大主題 SB-2「存取層」 的開篇。上一篇《RLS 效能最佳化與陷阱》幫你把資料庫層的安全政策寫得又對又快;這一篇則把鏡頭往上拉一層,帶你認識這些政策是透過什麼管道被使用的——PostgREST 自動 API。
核心觀念濃縮成一句話:在 Supabase 裡,設計好資料表就等於發布好 API——PostgREST 讀你的 schema、把每張表變成 /rest/v1/<table> 端點,而這條 REST 通道和 supabase-js 一樣,都受你設定的 RLS 保護。
- 建表即有 API:PostgREST 讀系統目錄,自動把 table/view/function 映射成 REST 端點,schema 一改數秒內自動重載
- 兩個 header:
apikey回答「哪個應用」(放 anon key)、Authorization: Bearer回答「哪個使用者」(放 JWT) - REST 一樣受 RLS 保護:anon key 受 RLS 限制、可安全放前端;service_role 繞過 RLS、只能放後端
- 兩種寫法同源:
curl直呼與 supabase-js 底層是同一套 REST,.from().select()就是在組 HTTP 請求 - schema reload:新表看不到時,用
notify pgrst, 'reload schema';手動重載快取
現在你已經看懂「一張表如何變成一個端點、怎麼帶對 header 安全存取」了。但真正的應用裡,讀取幾乎不會只是「把整張表撈出來」——你需要精準地篩選(只要符合條件的)、排序、以及在大量資料裡分頁。下一篇《查詢、篩選與分頁》,我們就深入 PostgREST 這套強大的查詢語法:eq、gt、ilike、in 等運算子怎麼用,limit/offset 與游標分頁各適合什麼場景,讓你把這套自動 API 用得又準又快。下一篇見。