Supabase Edge Functionsで始めるサーバーレス開発
Supabaseは、Firebase代替のオープンソースBaaS(Backend as a Service)として急速に人気を集めています。その中でも特に注目されているのが Supabase Edge Functions です。これは、Deno Deployをベースにしたサーバーレス関数実行環境で、グローバルに分散されたエッジロケーションでコードを実行できます。
本記事では、Supabase Edge Functionsの特徴、セットアップ方法、実用例、ベストプラクティスについて詳しく解説します。
Supabase Edge Functionsとは
Supabase Edge Functionsは、Denoランタイムで動作するサーバーレス関数です。世界中のエッジロケーションで実行されるため、低レイテンシーでグローバルなアプリケーションを構築できます。
主な特徴
- グローバル分散: 世界中のエッジロケーションで実行
- Denoランタイム: TypeScriptネイティブサポート、セキュアな実行環境
- Supabase統合: データベース、認証、ストレージと簡単に連携
- 無料枠: 月50万リクエストまで無料
- 低コールドスタート: 高速な起動時間
- Web標準API: Fetch API、Web Streams、Web Cryptoなど
セットアップ
前提条件
- Node.js 18以上
- Supabaseアカウント
- Supabase CLI
Supabase CLIのインストール
# npmでインストール
npm install -g supabase
# Homebrewでインストール(macOS/Linux)
brew install supabase/tap/supabase
# 確認
supabase --version
プロジェクトの初期化
# 新しいディレクトリを作成
mkdir my-supabase-project
cd my-supabase-project
# Supabaseプロジェクトを初期化
supabase init
# ローカル開発環境を起動
supabase start
Edge Functionの作成
# 新しいEdge Functionを作成
supabase functions new hello-world
これにより、supabase/functions/hello-world/index.ts ファイルが作成されます。
基本的なEdge Function
Hello World
// supabase/functions/hello-world/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
serve(async (req) => {
const { name } = await req.json();
const data = {
message: `Hello ${name || 'World'}!`,
};
return new Response(
JSON.stringify(data),
{
headers: { "Content-Type": "application/json" },
status: 200,
},
);
});
ローカルでのテスト
# Edge Functionをローカルで実行
supabase functions serve hello-world
# 別のターミナルでテスト
curl -X POST http://localhost:54321/functions/v1/hello-world \
-H "Content-Type: application/json" \
-d '{"name": "Alice"}'
デプロイ
# Supabaseにログイン
supabase login
# プロジェクトをリンク
supabase link --project-ref <your-project-ref>
# デプロイ
supabase functions deploy hello-world
Supabaseクライアントの使用
Edge Functions内でSupabaseデータベースやサービスにアクセスできます。
データベース操作
// supabase/functions/get-users/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
const supabaseKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
serve(async (req) => {
try {
const supabase = createClient(supabaseUrl, supabaseKey);
// ユーザー一覧を取得
const { data: users, error } = await supabase
.from('users')
.select('*')
.limit(10);
if (error) throw error;
return new Response(
JSON.stringify({ users }),
{
headers: { "Content-Type": "application/json" },
status: 200,
},
);
} catch (error) {
return new Response(
JSON.stringify({ error: error.message }),
{
headers: { "Content-Type": "application/json" },
status: 400,
},
);
}
});
認証の統合
// supabase/functions/protected-route/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
serve(async (req) => {
const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
const supabaseKey = Deno.env.get('SUPABASE_ANON_KEY')!;
// リクエストからJWTトークンを取得
const authHeader = req.headers.get('Authorization');
if (!authHeader) {
return new Response(
JSON.stringify({ error: 'Missing authorization header' }),
{ status: 401 },
);
}
const supabase = createClient(supabaseUrl, supabaseKey, {
global: {
headers: { Authorization: authHeader },
},
});
// ユーザー認証を確認
const { data: { user }, error } = await supabase.auth.getUser();
if (error || !user) {
return new Response(
JSON.stringify({ error: 'Unauthorized' }),
{ status: 401 },
);
}
// 認証済みユーザーのデータを返す
return new Response(
JSON.stringify({
message: 'Protected data',
user: {
id: user.id,
email: user.email,
},
}),
{
headers: { "Content-Type": "application/json" },
status: 200,
},
);
});
実践例
1. 画像リサイズAPI
// supabase/functions/resize-image/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
import { Image } from "https://deno.land/x/imagescript@1.2.15/mod.ts";
serve(async (req) => {
try {
const { imageUrl, width, height } = await req.json();
// 画像を取得
const response = await fetch(imageUrl);
const imageBuffer = await response.arrayBuffer();
// 画像をリサイズ
const image = await Image.decode(new Uint8Array(imageBuffer));
const resized = image.resize(width, height);
const encoded = await resized.encodeJPEG(80);
// Supabase Storageに保存
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
);
const fileName = `resized-${Date.now()}.jpg`;
const { data, error } = await supabase.storage
.from('images')
.upload(fileName, encoded, {
contentType: 'image/jpeg',
});
if (error) throw error;
const { data: { publicUrl } } = supabase.storage
.from('images')
.getPublicUrl(fileName);
return new Response(
JSON.stringify({ url: publicUrl }),
{
headers: { "Content-Type": "application/json" },
status: 200,
},
);
} catch (error) {
return new Response(
JSON.stringify({ error: error.message }),
{ status: 500 },
);
}
});
2. Stripe Webhookハンドラ
// supabase/functions/stripe-webhook/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
import Stripe from "https://esm.sh/stripe@14.10.0?target=deno";
const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY')!, {
apiVersion: '2023-10-16',
});
const webhookSecret = Deno.env.get('STRIPE_WEBHOOK_SECRET')!;
serve(async (req) => {
const signature = req.headers.get('stripe-signature');
if (!signature) {
return new Response('Missing signature', { status: 400 });
}
try {
const body = await req.text();
// Webhookイベントを検証
const event = stripe.webhooks.constructEvent(
body,
signature,
webhookSecret
);
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
);
// イベントタイプに応じて処理
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object;
// データベースを更新
await supabase
.from('subscriptions')
.insert({
user_id: session.client_reference_id,
stripe_customer_id: session.customer,
stripe_subscription_id: session.subscription,
status: 'active',
});
break;
}
case 'customer.subscription.deleted': {
const subscription = event.data.object;
// サブスクリプションをキャンセル
await supabase
.from('subscriptions')
.update({ status: 'canceled' })
.eq('stripe_subscription_id', subscription.id);
break;
}
}
return new Response(
JSON.stringify({ received: true }),
{ status: 200 },
);
} catch (error) {
return new Response(
JSON.stringify({ error: error.message }),
{ status: 400 },
);
}
});
3. メール送信API
// supabase/functions/send-email/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
const RESEND_API_KEY = Deno.env.get('RESEND_API_KEY')!;
serve(async (req) => {
try {
const { to, subject, html } = await req.json();
// Resend APIを使用してメール送信
const response = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${RESEND_API_KEY}`,
},
body: JSON.stringify({
from: 'noreply@yourdomain.com',
to: [to],
subject,
html,
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(error);
}
const data = await response.json();
return new Response(
JSON.stringify({ success: true, id: data.id }),
{
headers: { "Content-Type": "application/json" },
status: 200,
},
);
} catch (error) {
return new Response(
JSON.stringify({ error: error.message }),
{ status: 500 },
);
}
});
4. OpenAI統合
// supabase/functions/ai-chat/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
const OPENAI_API_KEY = Deno.env.get('OPENAI_API_KEY')!;
serve(async (req) => {
try {
const { message, userId } = await req.json();
// OpenAI APIを呼び出し
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${OPENAI_API_KEY}`,
},
body: JSON.stringify({
model: 'gpt-4',
messages: [
{
role: 'system',
content: 'You are a helpful assistant.',
},
{
role: 'user',
content: message,
},
],
}),
});
const data = await response.json();
const reply = data.choices[0].message.content;
// 会話履歴を保存
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
);
await supabase.from('chat_history').insert({
user_id: userId,
message,
reply,
});
return new Response(
JSON.stringify({ reply }),
{
headers: { "Content-Type": "application/json" },
status: 200,
},
);
} catch (error) {
return new Response(
JSON.stringify({ error: error.message }),
{ status: 500 },
);
}
});
CORS設定
Edge Functionsでは、CORSヘッダーを手動で設定する必要があります。
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};
serve(async (req) => {
// プリフライトリクエストへの対応
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders });
}
try {
// メイン処理
const data = { message: 'Hello' };
return new Response(
JSON.stringify(data),
{
headers: { ...corsHeaders, "Content-Type": "application/json" },
status: 200,
},
);
} catch (error) {
return new Response(
JSON.stringify({ error: error.message }),
{
headers: { ...corsHeaders, "Content-Type": "application/json" },
status: 400,
},
);
}
});
環境変数の管理
# ローカル環境変数の設定
supabase secrets set MY_SECRET_KEY=your-secret-value --env-file .env.local
# 本番環境への設定
supabase secrets set MY_SECRET_KEY=your-secret-value
.env.local ファイル:
OPENAI_API_KEY=sk-...
STRIPE_SECRET_KEY=sk_test_...
RESEND_API_KEY=re_...
クライアントからの呼び出し
JavaScriptクライアント
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
'https://your-project.supabase.co',
'your-anon-key'
);
// Edge Functionを呼び出し
const { data, error } = await supabase.functions.invoke('hello-world', {
body: { name: 'Alice' },
});
if (error) {
console.error('Error:', error);
} else {
console.log('Response:', data);
}
認証付きリクエスト
// ユーザーがログインしている場合、自動的にJWTトークンが送信される
const { data, error } = await supabase.functions.invoke('protected-route');
ベストプラクティス
1. エラーハンドリング
serve(async (req) => {
try {
// メイン処理
const result = await performOperation();
return new Response(
JSON.stringify({ success: true, data: result }),
{ status: 200 },
);
} catch (error) {
console.error('Error:', error);
return new Response(
JSON.stringify({
success: false,
error: error.message,
}),
{ status: 500 },
);
}
});
2. タイムアウト設定
// タイムアウト付きのfetch
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch(url, {
signal: controller.signal,
});
clearTimeout(timeoutId);
} catch (error) {
if (error.name === 'AbortError') {
// タイムアウト処理
}
}
3. パフォーマンス最適化
// 並列処理
const [users, posts, comments] = await Promise.all([
supabase.from('users').select('*'),
supabase.from('posts').select('*'),
supabase.from('comments').select('*'),
]);
// キャッシング
const cacheKey = `user-${userId}`;
const cached = await kv.get(cacheKey);
if (cached) {
return new Response(JSON.stringify(cached), { status: 200 });
}
const data = await fetchUserData(userId);
await kv.set(cacheKey, data, { ex: 3600 }); // 1時間キャッシュ
まとめ
Supabase Edge Functionsは、グローバルに分散されたサーバーレス関数を簡単に構築できる強力なツールです。Denoランタイムによる高速な実行、TypeScriptネイティブサポート、Supabaseエコシステムとのシームレスな統合により、モダンなバックエンド開発を効率化できます。
主な利点:
- グローバルエッジでの低レイテンシー実行
- TypeScriptネイティブサポート
- Supabaseとの統合が容易
- 無料枠が充実
注意点:
- Deno特有の制約(Node.jsモジュールの一部が未対応)
- コールドスタートの可能性
- 実行時間の制限
Supabase Edge Functionsを活用して、スケーラブルで高速なアプリケーションを構築してみてください。