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

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 をかなり読み込んだが、公式の説明はおおよそ次のとおりだ。

  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 header を検証し、認可されたユーザーだけが呼び出せるようにする。公開アクセスにしたい場合は、デプロイ時に --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: '[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)

いくつかのポイント。

  1. jsr:@hono/hono は Deno の JSR パッケージ管理形式で、npm ではない。JSR は Deno 公式のパッケージレジストリだ。
  2. basePath('/api') でルートのプレフィックスを /api にできる。
  3. c は Hono の context オブジェクトで、リクエスト・レスポンスと各種ユーティリティメソッドを含む。
  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 だ。この 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 のコールドスタートは速いとはいえ、できることはまだある。

  1. 依存のサイズを減らす:できるだけ Deno/JSR ネイティブのパッケージを使い、npm パッケージを減らす
  2. 遅延読み込み:大きなモジュールは必要に応じて import() する
  3. 関数を軽量に保つ: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

    ステップ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 でテストリクエストを送れます。

4分で読めます · 公開日: 2026年4月19日 · 更新日: 2026年6月1日

関連記事

コメント

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