言語を切り替える
テーマを切り替える

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 を長時間調査しました。公式の説明は概ね以下の通りです:

  1. 高速な起動:Deno は ESZip 形式でコードをパッケージ化し、関数のコールドスタートを 0-5ms で実現できます。一方、Node.js の Lambda のコールドスタートは通常 100-500ms です。

  2. セキュリティモデル:Deno はデフォルトでファイルシステムアクセスとネットワークアクセスを無効化し、明示的な許可が必要です。これはマルチテナントのエッジ環境で重要です。他の誰かの関数があなたのデータを読み取ることを望まないでしょう?

  3. TypeScript ネイティブサポート:tsconfig を設定したり、ts-node をインストールしたりする必要がありません。.ts ファイルを直接実行できます。長年 TypeScript でバックエンドを書いてきた人にとって、これは多くの設定時間を節約します。

  4. ポータビリティ: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.tomlfunctions/ サブディレクトリが含まれます(存在しない場合は自動作成されます)。

最初の 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 で、リクエスト処理関数を受け取ります。RequestResponse は標準の 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)

いくつかのポイント:

  1. jsr:@hono/hono は Deno の JSR パッケージ管理形式で、npm ではありません。JSR は Deno 公式のパッケージリポジトリです。
  2. basePath('/api') でルーティングのプレフィックスを /api に設定します。
  3. c は Hono のコンテキストオブジェクトで、リクエスト、レスポンス、各種ユーティリティメソッドを含みます。
  4. 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 のコールドスタートは高速ですが、さらにできることがあります:

  1. 依存関係のサイズを減らす:Deno/JSR ネイティブパッケージを優先し、npm パッケージの使用を最小限に
  2. 遅延ロード:大きなモジュールは必要に応じて import()
  3. 関数を軽量に保つ: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

    ステップ1: Supabase CLI をインストールしてログイン

    Homebrew を使って CLI をインストール(macOS):

    ```bash
    brew install supabase/tap/supabase
    supabase login
    ```

    ログイン後、ブラウザが開き CLI が Supabase アカウントへのアクセスを認可します。
  2. 2

    ステップ2: プロジェクトを初期化して関数を作成

    プロジェクトディレクトリで初期化コマンドを実行し、最初の関数を作成:

    ```bash
    supabase init
    supabase functions new hello-world
    ```

    これにより `supabase/functions/` ディレクトリに関数テンプレートが作成されます。
  3. 3

    ステップ3: ローカル開発環境を起動

    まずローカル Supabase スタック(PostgreSQL を含む)を起動し、その後関数サービスを起動:

    ```bash
    supabase start
    supabase functions serve --env-file supabase/.env.local
    ```

    ローカル関数のアドレス:`http://localhost:54321/functions/v1/{function-name}`
  4. 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

    ステップ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

    ステップ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 の違いは何ですか?
どちらもエッジコンピューティングプラットフォームですが、いくつかの違いがあります:(1) Edge Functions は Supabase のデータベース、認証、ストレージと深く統合されており、すぐに使えます;(2) Deno ランタイム vs V8 エンジン、Edge Functions は npm specifiers と JSR をサポート;(3) コールドスタートはどちらもミリ秒級で、性能は同等です。既に Supabase を使用している場合、Edge Functions の統合体験はより優れています。
Edge Functions は Webhook 処理に適していますか?
非常に適しています。Webhook 処理は Edge Functions の典型的なユースケースです:(1) コールドスタートが速く、Stripe や GitHub などの Webhook がタイムアウトしません;(2) 署名検証、ビジネスロジック処理、Supabase データベースへの書き込みを直接行えます;(3) --no-verify-jwt でデプロイして公開 API として使用可能です。
Deno と Node.js のパッケージ管理の違いは何ですか?
Deno は JSR(Deno 公式パッケージリポジトリ)と npm specifiers を使用します:(1) JSR パッケージは `jsr:@hono/hono` 形式でインポート;(2) npm パッケージは `npm:zod` 形式;(3) package.json と node_modules が不要で、依存関係は自動管理されます。主要なライブラリのほとんどがサポートされています。
Edge Functions で Supabase データベースに接続する方法は?
@supabase/supabase-js クライアントを使用します:(1) `jsr:@supabase/supabase-js@2` をインポート;(2) 環境変数から SUPABASE_URL と SUPABASE_SERVICE_ROLE_KEY を読み取り;(3) クライアント作成時に auth.autoRefreshToken: false を設定(エッジ環境ではリフレッシュ不要);(4) SERVICE_ROLE_KEY は RLS をバイパスするため、本番環境では権限管理に注意。
Edge Functions に実行時間制限はありますか?
あります。Supabase 公式は単一関数の実行時間を2秒以内、コールドスタート時間を 0-5ms にすることを推奨しています。短いライフサイクルの操作(Webhook、AI 推論、メール送信)に適しており、長時間実行タスク(動画トランスコード、大規模データ処理)には適していません。
Edge Functions をデバッグする方法は?
いくつかの方法があります:(1) ローカル `supabase functions serve` + ホットリロード、コード変更が即座に反映;(2) `console.log()` 出力は Dashboard のログに表示;(3) `supabase functions serve --env-file` でローカル環境変数をロード;(4) curl や Postman でテストリクエストを送信。

6 min read · 公開日: 2026年4月19日 · 更新日: 2026年4月19日

関連記事

コメント

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