Supabase Edge Functions 実践ガイド:Deno ランタイムと TypeScript 開発入門
深夜3時、スマホが激しく振動し始めました。本番環境で Stripe Webhook が 500 エラーを連発。顧客の支払いは成功したのに、注文が作成されていないのです。
慌ててログを確認すると、原因は以前のサーバーレス関数にありました。コールドスタートが長すぎて、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 ヘッダーを検証し、認可されたユーザーのみが呼び出せるようにします。公開アクセスしたい場合は、デプロイ時に --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: 'demo@example.com' } })
})
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 のコンテキストオブジェクトで、リクエスト、レスポンス、各種ユーティリティメソッドを含みます。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 を使用していることに注意してください。このキーは完全なデータベース権限を持ち、RLS(Row Level Security)をバイパスします。本番環境では慎重に使用してください。
エラーハンドリングとバリデーション
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>を持つリクエストのみが通過します - トークンに含まれるユーザー情報は
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 や他のモニタリングサービスと統合することも可能です。
また、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 をデバッグする方法は?
6 min read · 公開日: 2026年4月19日 · 更新日: 2026年4月19日
Supabase 実践ガイド
検索からこのページに来た場合は、前後の記事もあわせて読むと同じテーマの理解がかなり早く深まります。
前の記事
Supabase Storage 実践ガイド:ファイルアップロード、権限制御とCDN活用
Supabase Storageの完全な使い方を学びましょう。ファイルアップロードから権限設定、CDN連携まで、RLS、ユーザー分離、Smart CDN、画像変換を網羅した実践ガイド
第 4 / 7 記事
次の記事
Supabase Storage 実践ガイド:ファイルアップロード、CDN、アクセス制御
Supabase Storage の完全実践ガイド:3つのアクセス制御モードの比較、TUS 分割アップロード、Smart CDN 最適化テクニック、R2/S3 との価格比較分析。React コード例とトラブルシューティングを含みます。
第 6 / 7 記事
関連記事
Supabase 入門ガイド:PostgreSQL + Auth + Storage でオールインワン バックエンド
Supabase 入門ガイド:PostgreSQL + Auth + Storage でオールインワン バックエンド
Supabase データベース設計:テーブル構造、リレーションとRLS完全ガイド
Supabase データベース設計:テーブル構造、リレーションとRLS完全ガイド
Supabase Auth 実践ガイド:メール認証、OAuth、セッション管理

コメント
GitHubアカウントでログインしてコメントできます