Supabase Edge Functions 入門:第一個 Deno 無伺服器函式 | Supabase 完整教學
前一篇我們替 Realtime 補上授權、做出安全的即時聊天室,替「資料與即時」畫下句點。這一篇正式踏入 運算與 AI:當你需要在伺服器端跑自訂邏輯——呼叫第三方 API、把私密金鑰藏起來、處理 Webhook——就輪到 Edge Functions 登場。本篇帶你認識以 Deno 為底層的無伺服器(serverless) TypeScript 函式,從
supabase functions new建立、用Deno.serve寫 handler、本機supabase functions serve測試,到用supabase.functions.invoke從前端呼叫,寫出你的第一個部署到全球邊緣網路的函式,並釐清它與 Database Functions 的分工。
前言
一句話定義本篇主題:Edge Functions 是 Supabase 提供的無伺服器運算能力——你用 TypeScript 寫一個接收 Request、回傳 Response 的函式,它跑在以 Deno 為底層、部署至全球邊緣節點的執行環境裡,能以極低延遲回應世界各地的請求,並深度整合 Supabase 生態系與任何第三方 API。
先接住前面的脈絡。SB-3 我們把 Realtime 三大能力(Postgres Changes、Broadcast、Presence)加上授權練齊,那些邏輯幾乎都在資料庫與客戶端之間流動。但真實產品裡,總有一類需求沒辦法只靠資料庫和前端解決,例如:
- 要呼叫 OpenAI 生成摘要、呼叫 Stripe 建立結帳、呼叫寄信服務發送 email——這些都需要一個你能信任的伺服器端去代打。
- API 金鑰不能寫在前端。任何寫進瀏覽器 JavaScript 的金鑰都等於公開,
OPENAI_API_KEY、STRIPE_API_KEY必須藏在伺服器。 - 要接收外部服務的 Webhook(例如 Stripe 付款完成通知),需要一個公開可達的 HTTP 端點去驗證與處理。
過去這類需求你得自己架一台伺服器、或開一個 AWS Lambda。Supabase 把這件事變簡單了——這就是 Edge Functions。
用一個生活類比:把 Supabase 想成一間餐廳。資料庫(Postgres) 是後場的冷藏庫與食材,Realtime 是傳菜的即時通道,而 Edge Function 就是外場那位能跑腿的服務生——客人(前端)點了一杯「需要去隔壁店買原料才能做的特調」(呼叫第三方 API),服務生拿著只有店家才有的 VIP 卡(藏在伺服器的金鑰)去採買、調好、端回來。客人全程碰不到那張 VIP 卡,也不必知道原料哪裡來。
本篇你會學到:
- Deno runtime:Edge Function 的執行環境為什麼是 Deno 而不是 Node.js,這對你寫程式碼有什麼實際影響
- 建立與本機開發:
supabase functions new建立函式、Deno.serve寫 handler、supabase functions serve本機熱重載測試 - 從前端呼叫:用
supabase.functions.invoke('name', { body })呼叫函式,以及 CORS 這關為什麼一定要處理 - 何時該用 Edge Function:呼叫第三方 API、隱藏金鑰、伺服器邏輯——以及它與 Database Functions 的分工界線
本篇是 SB-4 運算與 AI 的首篇,聚焦「入門」。Secrets 管理、部署、背景任務、Cron、串流等進階主題會在後續篇章展開。
核心概念
要把 Edge Functions 用對,先建立三張心智圖:一張是「Edge Function 在整體架構中的位置」,一張是「Deno runtime 帶來的差異」,一張是「Edge Function vs Database Function 怎麼選」。
一、Edge Function 在架構中的位置
先看一個請求從前端到 Edge Function 的路徑:
[使用者瀏覽器 / App]
│ supabase.functions.invoke('my-fn', { body })
▼
[Edge Gateway:驗證 JWT、套用流量規則、限速]
│
▼
[最近的邊緣節點執行你的 Deno 函式]
│ 在這裡你可以…
├──▶ 用 service_role / anon key 讀寫 Postgres(可繞過或遵守 RLS)
├──▶ 帶著藏起來的金鑰呼叫第三方 API(OpenAI / Stripe / 寄信)
└──▶ 做任何 TypeScript 邏輯(驗證、轉換、編排)
│
▼
[回傳 Response 給前端]
三個要點:
- 它是一個標準的 HTTP 端點。 每個 Edge Function 部署後都有一個 URL(
https://<PROJECT_ID>.supabase.co/functions/v1/<function-name>),你可以用supabase.functions.invoke()、fetch、curl、或讓外部服務(Webhook)打它。 - 它「就地」整合 Supabase 生態系。 函式執行環境會自動注入一批環境變數(如
SUPABASE_URL、SUPABASE_SERVICE_ROLE_KEY),你不必手動設定就能在函式裡建立 Supabase 客戶端讀寫資料庫。 - 它跑在「邊緣」。 相比集中式的傳統伺服器,Edge Functions 分散部署在全球節點,請求會路由到離使用者最近的節點執行,延遲更低。
二、Deno runtime:和 Node.js 的關鍵差異
Edge Functions 的執行環境是 Deno,不是 Node.js。這是入門時最容易誤會、也最需要先建立的觀念。Deno 的關鍵特性:
| 特性 | 說明 | 對你的影響 |
|---|---|---|
| TypeScript 原生支援 | 無需 tsconfig.json 或編譯步驟,直接執行 .ts | 寫完就跑,開發體驗輕快 |
| Web 標準 API | 用標準 Request/Response、fetch、ReadableStream | handler 收 Request 回 Response,和瀏覽器同一套 |
| 套件匯入用 URL/前綴 | 支援 npm:、jsr:、esm.sh,無需 package.json | import ... from 'npm:zod' 直接載入 |
| 安全沙箱 | 預設停用檔案系統與網路存取,需明確授權 | 別預期能隨意讀寫本機檔案 |
| 隔離架構冷啟動極快 | isolate-based,冷啟動約 0–5ms | 適合高頻、短生命週期的請求 |
最重要的一句話:Deno 不是 Node,別假設 Node 的一切都能用。 具體來說:
- 可以用的:純 JS/TS 的 npm 套件(
npm:@supabase/supabase-js、npm:zod、npm:stripe等)、所有 Web 標準 API(fetch、crypto、URL、TextEncoder…)。 - 不能/不建議用的:依賴 Node 原生模組的套件(
fs檔案系統、net等)、需要原生二進位或多執行緒的套件(sharp/libvips影像處理)、以及假設 Node global 行為的舊套件。
所以在 Edge Function 裡,你發 HTTP 請求用的是全域的 fetch(Web 標準),而不是 Node 的 http/axios——這也是本篇範例會反覆出現 fetch 的原因。
三、Edge Function vs Database Function 怎麼選
這是入門就要建立、否則之後會一直踩的分界。兩者都是「伺服器端跑邏輯」,但適用場景完全不同:
| 維度 | Edge Function(Deno) | Database Function(PL/pgSQL) |
|---|---|---|
| 執行位置 | Deno 邊緣節點(資料庫之外) | Postgres 內部(離資料最近) |
| 語言 | TypeScript(完整程式語言) | SQL/PL/pgSQL |
| 最擅長 | 呼叫第三方 API、藏金鑰、Webhook、複雜編排 | 資料驗證、彙總計算、trigger、複雜查詢封裝 |
| 能藏 API 金鑰 | ✅(金鑰放伺服器環境變數) | ⚠️ 不適合(存取外部世界很彆扭) |
| 交易/RLS 整合 | 需透過客戶端呼叫 | ✅ 天生可包在交易裡、被 RLS/trigger 直接使用 |
| 網路往返 | 客戶端 → Edge → DB | 客戶端 → DB(就地完成) |
一個好記的判準:
邏輯若「圍繞資料、能用 SQL 表達、要在資料庫層就地完成」→ Database Function;邏輯若「要呼叫外部世界、需要程式語言彈性、要藏金鑰」→ Edge Function。
例如「計算某使用者本月訂單總額」——一句 SQL 就能做完,用 Database Function 最自然、最快,還能被 RLS 直接管。反過來「拿文章內容去呼叫 OpenAI 生成摘要,再存回資料庫」——這需要藏 OPENAI_API_KEY、需要 fetch 外部 API、需要程式語言的錯誤處理,就非 Edge Function 莫屬。把彙總硬塞進 Edge Function 只會多繞一次網路往返;把呼叫外部 API 硬塞進 Database Function 則會讓可維護性變差。
實作範例
下面用可直接執行的指令與 TypeScript,走一遍「建立函式 → 寫 handler → 本機測試 → 前端呼叫」的完整入門流程。假設你已安裝 Supabase CLI 與 Docker,並在專案根目錄執行過 supabase init 與 supabase start。以下 API 名稱與行為細節請以官方文件為準。
一、建立第一個 Edge Function
用 CLI 建立一個名為 hello-world 的函式:
# 建立新函式:會產生 supabase/functions/hello-world/index.ts
supabase functions new hello-world
執行後專案會多出這樣的目錄結構:
supabase/
└── functions/
└── hello-world/
└── index.ts
打開 index.ts,預設內容大致長這樣——這就是一個最小的 Edge Function:
// supabase/functions/hello-world/index.ts
// Deno.serve:註冊一個 HTTP handler,收 Request、回 Response
Deno.serve(async (req: Request) => {
// 從請求 body 讀取 JSON(Web 標準 API,和瀏覽器 fetch 同一套)
const { name } = await req.json()
const data = {
message: `Hello ${name}!`,
}
// 回傳標準 Response 物件
return new Response(JSON.stringify(data), {
headers: { "Content-Type": "application/json" },
})
})
三個入門重點:
Deno.serve(handler)是進入點:它啟動一個 HTTP 伺服器,把每個進來的請求交給你的handler。handler是一個async (req: Request) => Response的函式。req是 Web 標準的Request:req.json()、req.text()、req.headers.get(...)、req.method、req.url這些都和你在瀏覽器用的一模一樣。- 回傳的是 Web 標準
Response:你可以new Response(...),或用更方便的Response.json({...})(自動設好Content-Type)。
二、寫一個「呼叫第三方 API」的實用 handler
hello-world 太簡單,看不出 Edge Function 的價值。我們改寫成一個真正需要伺服器端的例子:接收一個城市名稱,去呼叫第三方天氣 API,把結果整理後回傳。重點是——API 金鑰藏在伺服器,前端完全碰不到。
// supabase/functions/get-weather/index.ts
// 在 handler「外部」讀取環境變數與初始化:只執行一次,避免每次請求重建
const WEATHER_API_KEY = Deno.env.get("WEATHER_API_KEY")!
Deno.serve(async (req: Request) => {
// 統一用 try/catch 包住,未捕捉的例外會變成 500
try {
// 1) 只允許 POST
if (req.method !== "POST") {
return Response.json({ error: "Method not allowed" }, { status: 405 })
}
// 2) 讀取並驗證輸入
const { city } = await req.json()
if (!city || typeof city !== "string") {
return Response.json({ error: "缺少 city 參數" }, { status: 400 })
}
// 3) 帶著藏起來的金鑰呼叫第三方 API(用 Web 標準 fetch,不是 axios)
const url =
`https://api.example-weather.com/v1/current` +
`?key=${WEATHER_API_KEY}&q=${encodeURIComponent(city)}`
const apiRes = await fetch(url)
if (!apiRes.ok) {
return Response.json(
{ error: "天氣服務暫時無法使用" },
{ status: 502 },
)
}
const raw = await apiRes.json()
// 4) 只把前端需要的欄位整理後回傳(也順便過濾敏感資訊)
return Response.json({
city,
tempC: raw.current?.temp_c,
condition: raw.current?.condition?.text,
})
} catch (err) {
// 錯誤會出現在 Dashboard 的函式日誌
console.error("get-weather 失敗:", err)
return Response.json({ error: "Internal Server Error" }, { status: 500 })
}
})
這個範例濃縮了 Edge Function 的三大價值:藏金鑰(WEATHER_API_KEY 在伺服器)、呼叫外部 API(fetch)、伺服器端邏輯(驗證輸入、整理輸出、統一錯誤處理)。注意昂貴的初始化(讀環境變數、之後你會建立的 Supabase 客戶端)都放在 Deno.serve 外面,這樣冷啟動一次後就能重複使用。
本機測試時,
WEATHER_API_KEY要放進supabase/functions/.env(或用--env-file指定)。Secrets 的完整管理留給下一篇,這裡先知道「金鑰從Deno.env.get()讀、絕不寫死在程式碼」即可。
三、本機啟動與測試
寫好之後,先在本機跑起來。CLI 內建 Deno 執行環境與熱重載:
# 啟動所有函式的本機開發伺服器(含熱重載,改檔即時生效)
supabase functions serve
# 只啟動特定函式,並指定環境變數檔
supabase functions serve get-weather --env-file supabase/functions/.env
啟動後,函式的本機位址為 http://localhost:54321/functions/v1/<函式名稱>。用 curl 送一個請求測試(本機呼叫需帶上 supabase start 顯示的 anon key):
# 用環境變數存放 anon key 比較方便
export ANON_KEY="<你的 anon key,supabase start 會印出>"
curl -X POST "http://localhost:54321/functions/v1/get-weather" \
-H "Authorization: Bearer $ANON_KEY" \
-H "Content-Type: application/json" \
-d '{"city":"Taipei"}'
# 預期回傳類似:
# {"city":"Taipei","tempC":28.5,"condition":"Partly cloudy"}
如果請求成功、日誌沒有紅字,代表函式邏輯正確。console.log/console.error 的輸出會即時印在 supabase functions serve 的終端機(部署後則出現在 Dashboard 的函式日誌)。
四、從前端呼叫:supabase.functions.invoke
本機驗證通過後,換前端呼叫。用 supabase-js 的 functions.invoke() 最方便——它會自動帶上目前登入使用者的 JWT 與 apikey,你不必手動組標頭:
import { createClient } from "@supabase/supabase-js"
const supabase = createClient(
"https://your-project.supabase.co",
"YOUR_PUBLISHABLE_KEY", // 前端用的可公開金鑰
)
async function fetchWeather(city: string) {
// 第一個參數是函式名稱,body 會被序列化成 JSON 送出
const { data, error } = await supabase.functions.invoke("get-weather", {
body: { city },
})
if (error) {
// invoke 失敗(網路、CORS、非 2xx 回應、或函式拋錯)都會進這裡
console.error("呼叫函式失敗:", error.message)
return null
}
console.log("天氣:", data) // { city, tempC, condition }
return data
}
fetchWeather("Taipei")
invoke 的幾個入門重點:
- 第一個參數是函式名稱(不是完整 URL),supabase-js 會自動組出
/functions/v1/get-weather。 body會被序列化成 JSON,對應到 handler 裡的await req.json()。- JWT 自動帶入:登入使用者的 token 會自動附上;因此 handler 預設會通過 JWT 驗證。若要做公開端點(免登入),部署時用
--no-verify-jwt(下一篇會談)。
五、處理 CORS:瀏覽器呼叫的必修課
上面的 get-weather 從 curl 或伺服器端呼叫沒問題,但從瀏覽器呼叫時,你很可能會撞上 CORS(跨來源資源共享)。原因是瀏覽器在正式請求前會先送一個 OPTIONS preflight 探路,你的函式若不回應它、或回應沒帶對標頭,請求就會被瀏覽器擋下。
正確做法:在 handler 開頭處理 OPTIONS,並讓所有回應都帶上 CORS 標頭。把 CORS 標頭抽成共用常數:
// supabase/functions/_shared/cors.ts
// 以 _ 開頭的資料夾不會被部署為獨立函式,適合放共用模組
export const corsHeaders = {
"Access-Control-Allow-Origin": "*", // 正式環境建議指定具體網域
"Access-Control-Allow-Headers":
"authorization, x-client-info, apikey, content-type",
"Access-Control-Allow-Methods": "POST, OPTIONS",
}
在函式裡套用:
// supabase/functions/get-weather/index.ts(加上 CORS 版)
import { corsHeaders } from "../_shared/cors.ts"
const WEATHER_API_KEY = Deno.env.get("WEATHER_API_KEY")!
Deno.serve(async (req: Request) => {
// 1) 先回應瀏覽器的 preflight(OPTIONS),否則跨來源請求會被擋
if (req.method === "OPTIONS") {
return new Response("ok", { headers: corsHeaders })
}
try {
const { city } = await req.json()
if (!city) {
return Response.json(
{ error: "缺少 city 參數" },
{ status: 400, headers: corsHeaders }, // 2) 錯誤回應也要帶 CORS 標頭
)
}
const apiRes = await fetch(
`https://api.example-weather.com/v1/current` +
`?key=${WEATHER_API_KEY}&q=${encodeURIComponent(city)}`,
)
const raw = await apiRes.json()
// 3) 成功回應同樣帶上 CORS 標頭
return Response.json(
{ city, tempC: raw.current?.temp_c, condition: raw.current?.condition?.text },
{ headers: corsHeaders },
)
} catch (err) {
console.error("get-weather 失敗:", err)
return Response.json(
{ error: "Internal Server Error" },
{ status: 500, headers: corsHeaders },
)
}
})
記住一句話:只要函式會被瀏覽器直接呼叫,就一定要處理 OPTIONS 並在每個回應帶上 CORS 標頭。 「curl 通、瀏覽器不通」幾乎都是這關沒過。
常見錯誤與最佳實踐
坑一:把 Edge Function 當 Node.js 寫,直接搬 fs、axios、sharp。
Edge Function 跑在 Deno,不是 Node。import fs from 'fs' 讀本機檔案會失敗(沙箱預設關閉檔案系統)、sharp 這類需要原生多執行緒的影像套件不支援、部分假設 Node global 的舊套件也會爆。正確做法:發 HTTP 用 Web 標準 fetch(不是 axios/Node http);需要 npm 套件時用 npm: 前綴匯入(如 npm:@supabase/supabase-js@2),匯入優先序 npm: > jsr: > esm.sh;遇到套件裝不起來,先確認它是不是碰了 Node 原生能力。
坑二:把該用 SQL 的邏輯硬塞進 Edge Function。 「算某使用者本月訂單總額」一句 SQL 就能在資料庫就地完成,硬做成 Edge Function 反而多一次網路往返(客戶端 → Edge → DB → Edge → 客戶端),還失去交易與 RLS 的天然整合。正確做法:圍繞資料、能用 SQL 表達的邏輯(驗證、彙總、trigger)交給 Database Function;只有「要呼叫外部世界、要藏金鑰、要程式語言彈性」時才動用 Edge Function。選對工具比寫得漂亮更重要。
坑三:瀏覽器呼叫時沒處理 CORS,一直被擋。
函式在 curl 下正常,一放到網頁就 CORS error——因為沒回應 OPTIONS preflight,或回應沒帶 Access-Control-Allow-* 標頭。正確做法:handler 開頭處理 req.method === 'OPTIONS' 回一個帶 CORS 標頭的回應,並讓成功與錯誤的每個回應都帶上同一組 CORS 標頭(把它抽成 _shared/cors.ts 共用);正式環境把 Access-Control-Allow-Origin 從 * 收斂成你的具體網域。
坑四:把 API 金鑰寫死在程式碼、甚至寫進前端。
金鑰硬編碼進函式原始碼、或想省事直接放前端,等於公開你的付費 API 帳號。正確做法:金鑰一律用 Deno.env.get('XXX_KEY') 從環境變數讀取,本機放 supabase/functions/.env(記得加進 .gitignore),正式環境用 Secrets 管理(下一篇詳談);Edge Function 存在的一大理由就是當金鑰的保險箱,別讓它外洩。
坑五:不做錯誤處理,函式一拋錯就回神秘的 500。
未捕捉的例外會讓函式回傳 500,前端只看到「呼叫失敗」卻查不出原因。正確做法:用 try/catch 包住 handler 主體,回傳語意明確的狀態碼(400 輸入錯、401 未授權、502 上游 API 掛了、500 內部錯),並用 console.error 記錄細節——這些日誌會出現在 supabase functions serve 終端機與 Dashboard 的函式日誌,是你排查問題的主要依據。
坑六:每次請求都重新初始化昂貴資源。
把 createClient(...)、讀環境變數、建連線池都寫進 Deno.serve 的 handler 裡,每個請求都重做一次,浪費且變慢。正確做法:把只需做一次的初始化放在 Deno.serve 外面(模組層級),利用 Deno isolate 冷啟動後會重複使用實例的特性,讓客戶端與設定只建立一次。
最佳實踐總結:
- 記住這是 Deno 不是 Node:
fetch而非axios、npm:前綴匯入、別碰 Node 原生模組 - 選對工具:資料/SQL 邏輯用 Database Function,外部 API/藏金鑰/編排用 Edge Function
- 瀏覽器呼叫必處理 CORS:回應
OPTIONS、每個回應帶 CORS 標頭、正式環境收斂 origin - 金鑰只從環境變數讀:
Deno.env.get(),.env進.gitignore,讓 Edge Function 當保險箱 - 統一 try/catch 與語意化狀態碼:搭配
console.error/函式日誌排查 - 昂貴初始化放 handler 外:善用 isolate 重用,降低冷啟動後的開銷
- 不確定的 API 以官方為準:
Deno.serve、functions.invoke、CLI 指令、npm/jsr 支援細節都可能調整,正式決策前查官方文件
小結
這是 Supabase 系列教學的第 031 篇,也是 SB-4 運算與 AI 的開篇。上一篇《Realtime Authorization 與實戰》我們用 config: { private: true } 與 realtime.messages 的 RLS policy,做出安全、可上線的即時聊天室,替 SB-3 核心服務收尾。這一篇我們正式踏進伺服器端運算——認識以 Deno 為底層的 Edge Functions,從 supabase functions new 建立、用 Deno.serve 寫 handler、supabase functions serve 本機測試,到用 supabase.functions.invoke 從前端呼叫,並釐清了它與 Database Functions 的分工。
核心觀念濃縮成一句話:Edge Function 是跑在 Deno 邊緣節點的無伺服器 TypeScript 函式,用 Deno.serve 接收 Request、回傳 Response;它擅長呼叫第三方 API、隱藏金鑰、處理 Webhook 與伺服器編排(這些用 SQL 很彆扭),而圍繞資料、能用 SQL 表達的邏輯則交給 Database Function;因為底層是 Deno 不是 Node,要用 fetch 與 npm: 匯入、避開 Node 原生模組;只要被瀏覽器呼叫就一定要處理 CORS。
- 執行環境:Deno(TypeScript 原生、Web 標準 API、
npm:/jsr:/esm.sh匯入、無需package.json) - 入門四步:
functions new建立 →Deno.serve寫 handler →functions serve本機測試 →functions.invoke前端呼叫 - 何時用:呼叫第三方 API、藏金鑰、Webhook、複雜編排 → Edge Function;資料/SQL 邏輯 → Database Function
- 兩大必修:金鑰用
Deno.env.get()讀、瀏覽器呼叫要處理 CORS
你已經寫出並在本機跑通了第一個 Edge Function,但它還沒真正上線,金鑰也還躺在本機的 .env 裡。下一篇《Secrets、環境與部署》,我們要處理「怎麼把私密金鑰安全地交給正式環境的函式」以及「怎麼把函式部署到 Supabase 的全球邊緣網路」,讓你的第一個函式真正對外服務。運算篇章已經啟程,下一篇見。