Supabase Edge Functions 実践:Deno ランタイムと TypeScript 開発ガイド
スマホが激しく震える。Stripe の Webhook が本番環境で 500 エラーを連発していた。顧客の支払いは成功しているのに、注文が作成されない。
起き上がってログを調べると、問題は元の serverless 関数にあった——コールドスタートが長すぎて、Stripe が待ちきれずにタイムアウトしていたのだ。さらに厄介なことに、署名検証や CORS 処理のために API ゲートウェイをもう一式組まなければならなかった……。
あの夜以降、本気で Supabase Edge Functions を調べ始めた。最初は「Deno ランタイム」という文字を見て少しためらった——なにしろ Node.js を何年も書いてきたので、ランタイムを変えるということは API を一から学び直すことを意味する。だが触ってみると、Edge Functions の設計思想はまったく別物だった。それは「移行」させるためのものではなく、重量級の依存を必要としないシナリオ専用の、より軽量な選択肢を提供するものだったのだ。
この記事では、踏んだ落とし穴と学んだことを共有する。Edge Functions のアーキテクチャの原理、Deno と Node.js の違い、ローカル開発・デバッグの流れ、そして Hono フレームワークで API をスマートに書く実践経験だ。
Edge Functions とは何か —— アーキテクチャと技術選定
まずは Edge Functions が何なのか、そしてなぜ Supabase が Node.js ではなく Deno を選んだのかをはっきりさせよう。
エッジ実行であって、クラウドホスティングではない
Edge Functions はエッジノード上で動く TypeScript の関数だ。従来の Lambda や Vercel Functions と違い、いくつかの大きなリージョンのサーバーに集中デプロイされるのではなく、世界中の数百のエッジノードに分散している。
これは何を意味するのか。上海にいるユーザーがリクエストを発すると、関数は東京のエッジノードで実行されるかもしれない。レイテンシは数百ミリ秒から数十ミリ秒まで下がる。
ただしエッジには代償もある——関数は重すぎてはいけない。各関数は独立した V8 isolate 内で動き、それぞれ独自のメモリヒープと実行スレッドを持つ。起動速度はミリ秒級だが、メモリは限られ、実行時間にも制限がある。だから短いライフサイクルの処理に向く。Webhook 処理、OG 画像生成、サードパーティ API 呼び出し、メール送信などだ。
向かないものもある。長時間実行のタスク、大量の Node.js ネイティブモジュールに依存するライブラリ、ファイルシステムへのアクセスが必要な操作だ。
なぜ Deno なのか
この疑問について Supabase の GitHub Discussion をかなり読み込んだが、公式の説明はおおよそ次のとおりだ。
- 起動が速い:Deno は ESZip 形式でコードをパッケージングし、関数のコールドスタートを 0-5ms に抑えられる。一方 Node.js の Lambda のコールドスタートは通常 100-500ms だ。
- セキュリティモデル:Deno はデフォルトでファイルシステムアクセスとネットワークアクセスを無効化しており、明示的な認可が必要だ。これはマルチテナントのエッジ環境では重要——他人の関数に自分のデータを読まれたくないだろう?
- TypeScript ネイティブサポート:tsconfig を設定したり ts-node をインストールしたりせず、
.tsファイルをそのまま動かせる。早くから TypeScript でバックエンドを書いてきた人にとって、設定の時間がかなり節約できる。 - 可搬性:Deno は他のアプリに組み込める。Supabase は自前でメンテナンスする Deno のフォーク
deno_coreを使っており、組み込み用途向けに改良されている。
得るものがあれば失うものもある。Deno のエコシステムは Node.js よりかなり小さく、一部の npm パッケージはそのままでは使えない。ただし Deno は今や npm specifiers に対応しており、import { xxx } from 'npm:lodash' と書ける。互換性はかなり良くなった。
アーキテクチャ概観
リクエストが入ってきてからの流れは、おおよそこうだ。
クライアント → CDN/エッジゲートウェイ → JWT 検証 → V8 isolate で関数を実行 → レスポンス返却
ポイントはその JWT 検証だ——Edge Functions はデフォルトでリクエストの Authorization header を検証し、認可されたユーザーだけが呼び出せるようにする。公開アクセスにしたい場合は、デプロイ時に --no-verify-jwt フラグを付ける必要がある。
開発環境の構築と CLI コマンド詳解
さて、概念の説明は終わった。手を動かしていこう。
Supabase CLI のインストール
私は macOS を使っているので、Homebrew で直接インストールする。
brew install supabase/tap/supabase
Linux と Windows にも対応するインストール方法があり、公式ドキュメントに詳しく書かれているのでここでは繰り返さない。
インストールが終わったらログインする。
supabase login
このステップではブラウザが開き、CLI があなたの Supabase アカウントにアクセスする認可を求められる。
プロジェクトの初期化
プロジェクトディレクトリで実行する。
supabase init
これで supabase/ ディレクトリが作成される。中には設定ファイル config.toml と functions/ サブディレクトリ(存在しなければ自動生成される)がある。
最初の Edge Function を作成する
supabase functions new hello-world
このコマンドは supabase/functions/ の下に hello-world/ ディレクトリを作成し、中に index.ts ファイルを置く。こんな内容だ。
Deno.serve(async (req: Request) => {
const { name } = await req.json()
const data = {
message: `Hello ${name}!`,
}
return new Response(JSON.stringify(data), {
headers: {
'Content-Type': 'application/json',
'Connection': 'keep-alive',
},
})
})
そう、これだけシンプルだ。Deno.serve() は Deno のネイティブ API で、リクエスト処理関数を受け取る。Request と Response はどちらも標準の Web API で、ブラウザの fetch と同じ使い方だ。
ローカル開発サーバー
ローカル開発環境を起動する。
supabase functions serve --env-file supabase/.env.local
これでローカルサーバーが起動する。デフォルトのアドレスは http://localhost:54321 だ。関数は http://localhost:54321/functions/v1/hello-world でアクセスできる。
正直、初めて動かしたときに落とし穴にはまった——先に Supabase のローカルサービススタック(ローカル PostgreSQL を含む)を起動するのを忘れていたのだ。正しいやり方はこうだ。
# 先にローカルの Supabase スタックを起動
supabase start
# 次に関数サービスを起動
supabase functions serve
テストリクエスト
curl か HTTPie でリクエストを送って試してみよう。
curl -i --location --request POST 'http://localhost:54321/functions/v1/hello-world' \
--header 'Authorization: Bearer <your-anon-key>' \
--header 'Content-Type: application/json' \
--data '{"name":"World"}'
返ってくるのはこれだ。
{
"message": "Hello World!"
}
成功だ。
ホットリロードは自動で有効になっている。コードを変更して保存するとすぐに反映され、サービスを再起動する必要はない。この体験はかなり良い。
環境変数
機密情報をコードに書いてはいけない。Supabase は .env ファイルで環境変数を管理できる。
# .env ファイルを作成
echo "MY_SECRET=super_secret_value" > supabase/.env.local
# 関数内で読み取る
const mySecret = Deno.env.get('MY_SECRET')
本番環境にデプロイするときは supabase secrets set コマンドを使う。
supabase secrets set MY_SECRET=super_secret_value
実践:Hono フレームワークで RESTful API を構築する
ネイティブの Deno.serve() で十分だが、関数のロジックが複雑になり——ルーティング・ミドルウェア・パラメータ検証が必要になると——手書きはかなりつらくなる。
そこで Hono の出番だ。
Hono とは
Hono はエッジランタイム向けに設計された超軽量の Web フレームワークだ。Deno、Cloudflare Workers、Bun など複数のランタイムに対応し、ルーティング性能が高く、TypeScript サポートも一流だ。
公式は「small, simple, and ultrafast」と言っている——使ってみて、確かにそのとおりだと感じた。
Edge Functions への統合
まず新しい関数を作成する。
supabase functions new user-api
そして index.ts を修正する。
import { Hono } from 'jsr:@hono/hono'
import { cors } from 'jsr:@hono/hono/cors'
import { logger } from 'jsr:@hono/hono/logger'
const app = new Hono().basePath('/api')
// ミドルウェア
app.use('*', cors())
app.use('*', logger())
// ルート定義
app.get('/users/:id', (c) => {
const id = c.req.param('id')
return c.json({ user: { id, name: 'Demo User', email: '[email protected]' } })
})
app.post('/users', async (c) => {
const body = await c.req.json<{ name: string; email: string }>()
// ここで Supabase データベースに接続できる
return c.json({ created: body }, 201)
})
app.put('/users/:id', async (c) => {
const id = c.req.param('id')
const body = await c.req.json<{ name?: string; email?: string }>()
return c.json({ updated: { id, ...body } })
})
app.delete('/users/:id', (c) => {
const id = c.req.param('id')
return c.json({ deleted: id })
})
// サービスを起動
Deno.serve(app.fetch)
いくつかのポイント。
jsr:@hono/honoは Deno の JSR パッケージ管理形式で、npm ではない。JSR は Deno 公式のパッケージレジストリだ。basePath('/api')でルートのプレフィックスを/apiにできる。cは Hono の context オブジェクトで、リクエスト・レスポンスと各種ユーティリティメソッドを含む。c.json()は Content-Type ヘッダーを自動設定し、null や undefined も処理できる。
Supabase データベースへの接続
Hono は単なる Web フレームワークなので、データベースを操作するには Supabase クライアントが必要だ。完全な例を示す。
import { Hono } from 'jsr:@hono/hono'
import { createClient } from 'jsr:@supabase/supabase-js@2'
const app = new Hono().basePath('/api')
// Supabase クライアントを初期化
const supabaseUrl = Deno.env.get('SUPABASE_URL')!
const supabaseKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
const supabase = createClient(supabaseUrl, supabaseKey, {
auth: {
autoRefreshToken: false,
persistSession: false,
},
})
// GET /api/users - 一覧
app.get('/users', async (c) => {
const { data, error } = await supabase
.from('users')
.select('id, name, email, created_at')
if (error) {
return c.json({ error: error.message }, 500)
}
return c.json({ users: data })
})
// POST /api/users - 作成
app.post('/users', async (c) => {
const body = await c.req.json<{ name: string; email: string }>()
const { data, error } = await supabase
.from('users')
.insert(body)
.select()
.single()
if (error) {
return c.json({ error: error.message }, 400)
}
return c.json({ user: data }, 201)
})
Deno.serve(app.fetch)
ここで使っているのは SUPABASE_SERVICE_ROLE_KEY だ。この key は完全なデータベース権限を持ち、RLS をバイパスする。本番環境では慎重に使う必要がある。
エラー処理と検証
Hono には組み込みのバリデーターがないが、Zod と組み合わせて使える。
import { z } from 'npm:zod'
import { zValidator } from 'jsr:@hono/zod-validator'
const userSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
})
app.post(
'/users',
zValidator('json', userSchema),
async (c) => {
const validated = c.req.valid('json')
// validated はすでに型安全なオブジェクトになっている
return c.json({ received: validated })
}
)
検証に失敗すると自動的に 400 エラーが返り、レスポンスボディには詳細なエラー情報が含まれる。
デプロイと本番環境のベストプラクティス
ローカルで動くようになった。本番に上げよう。
デプロイコマンド
supabase functions deploy user-api
初回のデプロイでは、どの Supabase プロジェクトにリンクするか尋ねられる。その後は自動でコードのアップロード・ビルド・デプロイが行われる。
デプロイに成功すると、関数の URL の形式はこうなる。
https://[PROJECT_ID].supabase.co/functions/v1/user-api
環境変数と Secrets
本番環境の環境変数は個別に設定する必要がある。
supabase secrets set SUPABASE_URL=https://xxx.supabase.co
supabase secrets set SUPABASE_SERVICE_ROLE_KEY=eyJxxx...
これらの Secrets は暗号化されて保存され、関数実行時に Deno.env.get() で読み取られる。
JWT 検証戦略
前述のとおり、Edge Functions はデフォルトで JWT を検証する。これは次のことを意味する。
- 有効な
Authorization: Bearer <token>を持つリクエストだけが通過できる - Token に含まれるユーザー情報は
req.headersから解析できる
公開 API にしたい場合(たとえばサードパーティの Webhook 用)は、デプロイ時に --no-verify-jwt を付ける。
supabase functions deploy user-api --no-verify-jwt
ただしこれは誰でも関数を呼び出せることを意味するので、コード内で自前の検証を行う必要がある。
コールドスタートのレイテンシを減らす
Deno のコールドスタートは速いとはいえ、できることはまだある。
- 依存のサイズを減らす:できるだけ Deno/JSR ネイティブのパッケージを使い、npm パッケージを減らす
- 遅延読み込み:大きなモジュールは必要に応じて
import()する - 関数を軽量に保つ:1 つの関数は 1 つのことだけをする。バックエンド全体を詰め込まない
Supabase の公式は単一関数の実行時間を 2 秒以内に、コールドスタートを 0-5ms に抑えることを推奨している。だから以下の提案は、レスポンスをより速くするのに役立つはずだ。
モニタリングとログ
Dashboard では関数の呼び出しログとエラーレポートを確認できる。Sentry など他のモニタリングサービスを統合することもできる。
もう 1 つ EdgeRuntime.waitUntil() という API があり、関数がレスポンスを返した後もバックグラウンドタスクを継続実行させられる。
EdgeRuntime.waitUntil(
fetch('https://analytics.example.com/track', { method: 'POST', body: '...' })
)
return new Response('OK')
これでクライアントはバックグラウンドタスクの完了を待たずにレスポンスを受け取れる。
結論
ここまで色々語ってきたが、結局 Edge Functions はどんなシナリオに向いているのか。
向いているもの:
- Webhook 処理(Stripe、GitHub、Slack)
- OG 画像生成
- AI 推論(LLM API の呼び出し)
- メールやメッセージ通知
- 短いライフサイクルのデータ処理
あまり向かないもの:
- 長時間実行のタスク(動画トランスコードなど)
- 重量級の Node.js ネイティブモジュールに依存するライブラリ
- ファイルシステムへのアクセスが必要な操作
すでに Supabase のデータベースと認証を使っているなら、Edge Functions はとても自然な拡張だ——追加のサーバーを組む必要も、運用を気にする必要もなく、ビジネスロジックを書くだけでいい。
Cloudflare Workers や Vercel Functions と比べると?それぞれに強みがあると思う。Cloudflare Workers はより成熟していてエコシステムも大きく、Vercel Functions は Next.js のエコシステムとより深く結びついている。だがすでに Supabase を使っているなら、Edge Functions の統合体験が最も優れている——データベースクライアント、認証、ストレージがすべて揃っているからだ。
試してみたいなら、公式のサンプルリポジトリから始めるとよい:github.com/supabase/supabase/tree/master/examples/edge-functions
質問があればコメント欄に書き込むか、直接 Supabase Discord でコミュニティに助けを求めてほしい。
Supabase Edge Functions の開発・デプロイ完全フロー
環境構築から本番デプロイまでの完全な操作ガイド
⏱️ 目安時間: 45 分
- 1
ステップ1: Supabase CLI をインストールしてログインする
Homebrew で CLI をインストールします(macOS):
```bash
brew install supabase/tap/supabase
supabase login
```
ログインするとブラウザが開き、CLI があなたの Supabase アカウントにアクセスする認可が求められます。 - 2
ステップ2: プロジェクトを初期化して関数を作成する
プロジェクトディレクトリで初期化コマンドを実行し、最初の関数を作成します:
```bash
supabase init
supabase functions new hello-world
```
これで `supabase/functions/` ディレクトリに関数テンプレートが作成されます。 - 3
ステップ3: ローカル開発環境を起動する
まずローカルの Supabase スタック(PostgreSQL を含む)を起動し、続いて関数サービスを起動します:
```bash
supabase start
supabase functions serve --env-file supabase/.env.local
```
ローカル関数のアドレス:`http://localhost:54321/functions/v1/{function-name}` - 4
ステップ4: Hono フレームワークで API を構築する
Hono をインストールして RESTful API を作成します:
```typescript
import { Hono } from 'jsr:@hono/hono'
import { cors } from 'jsr:@hono/hono/cors'
const app = new Hono().basePath('/api')
app.use('*', cors())
app.get('/users/:id', (c) => {
return c.json({ user: { id: c.req.param('id') } })
})
Deno.serve(app.fetch)
```
Hono はルーティング・ミドルウェア・パラメータ検証に対応しています。 - 5
ステップ5: 環境変数と Secrets を設定する
ローカル開発では `.env` ファイルを、本番環境では Secrets を使います:
```bash
# ローカル
echo "MY_SECRET=value" > supabase/.env.local
# 本番
supabase secrets set MY_SECRET=value
```
関数内では `Deno.env.get('MY_SECRET')` で読み取ります。 - 6
ステップ6: 本番環境にデプロイする
関数をデプロイし、必要に応じて公開アクセスを設定します:
```bash
# 標準デプロイ(JWT 検証が必要)
supabase functions deploy user-api
# 公開 API(JWT 検証なし)
supabase functions deploy user-api --no-verify-jwt
```
本番 URL の形式:`https://[PROJECT_ID].supabase.co/functions/v1/user-api`
FAQ
Supabase Edge Functions と Cloudflare Workers の違いは何ですか?
Edge Functions は Webhook の処理に向いていますか?
Deno と Node.js のパッケージ管理はどう違いますか?
Edge Functions で Supabase データベースに接続するには?
Edge Functions に実行時間の制限はありますか?
Edge Functions をデバッグするには?
4分で読めます · 公開日: 2026年4月19日 · 更新日: 2026年6月1日
Supabase 実践ガイド
検索からこのページに来た場合は、前後の記事もあわせて読むと同じテーマの理解がかなり早く深まります。
前の記事
Supabase Storage 実践:ファイルアップロード、CDN、アクセス制御
Supabase Storage の完全実践ガイド。3 つのアクセス制御モードの比較、TUS 分割アップロード、Smart CDN の最適化テクニック、R2/S3 との価格比較を解説。React のコード例とトラブルシューティングも掲載。
第 7 / 10 記事
次の記事
Supabase Auth 詳細設定:OAuth・SSO・権限制御
Supabase Auth の高度な設定を徹底解説:OAuth マルチプロバイダー連携、SAML SSO による企業認証、RLS マルチテナント権限分離まで、コンシューマー向けアプリから企業 SaaS までをカバーする完全な認証ソリューション
第 9 / 10 記事
関連記事
Supabase 入門:PostgreSQL + Auth + Storage のオールインワンバックエンド
Supabase 入門:PostgreSQL + Auth + Storage のオールインワンバックエンド
Supabase データベース設計:テーブル構造・リレーション・Row Level Security 完全ガイド
Supabase データベース設計:テーブル構造・リレーション・Row Level Security 完全ガイド
Supabase Auth 実践:メール認証・OAuth・セッション管理
コメント
GitHubアカウントでログインしてコメントできます