Netlify Functions
プラットフォーム
Netlify Functions
概要
Netlify Functionsは、JAMstackアーキテクチャに最適化されたサーバーレス関数実行環境として、静的サイトホスティングとシームレスに統合されたエンドツーエンドのWebアプリケーション開発を可能にします。従来のNode.js Functionsと革新的なEdge Functions(Deno Runtime)の2つの実行環境を提供し、静的コンテンツ配信からAPI開発、認証、リアルタイム処理まで、フロントエンド中心の開発ワークフローを強力にサポートします。2025年現在、エッジコンピューティング、ストリーミング、TypeScript対応、GitOpsワークフローの統合により、モダンWebアプリケーション開発の標準プラットフォームとして急速に成長しています。
詳細
Netlify Functions 2025年版は、JAMstackエコシステムの中核として、静的サイトホスティングと密接に統合されたサーバーレスコンピューティング環境を提供しています。特に注目すべきは、Deno Runtimeをベースとした Edge Functions の導入により、全世界のエッジロケーションでの50-200ms以下の超高速実行、TypeScript ネイティブサポート、Web Standards準拠のAPI、ストリーミング対応を実現していることです。Git連携による自動デプロイ、プレビューデプロイ、A/Bテスト機能、Background Functions による長時間実行処理、そして統合された監視・ログ機能により、開発からプロダクションまでの完全なワークフローを提供します。
主な特徴
- Edge Functions: Deno Runtime による超高速エッジ実行(50ms以下)
- Node.js Functions: 従来型のサーバーレス関数(最大15分実行)
- JAMstack統合: 静的サイトとのシームレスな統合
- TypeScript対応: ネイティブTypeScriptサポート
- Git連携: GitOpsワークフローによる自動デプロイ
- ストリーミング: リアルタイムレスポンスストリーミング
2025年の最新機能
- Edge Functions: 50ms以下の高速実行とストリーミング対応
- Background Functions: 15分までの長時間非同期実行
- Enhanced Deploy Previews: ブランチ毎のプレビュー環境
- Integrated Monitoring: リアルタイム関数監視とログ分析
- Framework Integration: Astro、Next.js、Nuxt、SvelteKit等との深い統合
メリット・デメリット
メリット
- JAMstackアーキテクチャに特化した統合開発体験
- Edge Functionsによる世界最高水準の低遅延実行環境
- Git連携による開発ワークフローの完全自動化
- TypeScript/JavaScript両対応による開発者体験の向上
- 静的ホスティングとAPI開発の一元管理
- プレビューデプロイによる安全な開発プロセス
- 豊富なフレームワーク統合とテンプレート
デメリット
- Netlifyプラットフォームへの強い依存とベンダーロックイン
- Edge Functions の実行時間制限(50ms)による処理制約
- 他のクラウドプロバイダーと比較して限定的なバックエンド機能
- 複雑なデータベース操作や重い処理には不向き
- エンタープライズレベルの高度な運用機能の不足
- 従量課金制による予期しないコスト増加の可能性
参考ページ
書き方の例
セットアップと関数作成
# Netlify CLIのインストール
npm install -g netlify-cli
# Netlifyへのログイン
netlify login
# 新しいプロジェクトの初期化
netlify init
# ローカル開発サーバーの起動
netlify dev
# 関数の作成
netlify functions:create
// netlify/functions/hello.js - 基本的なNode.js関数
exports.handler = async (event, context) => {
console.log('Event:', JSON.stringify(event, null, 2));
console.log('Context:', JSON.stringify(context, null, 2));
// CORSヘッダーの設定
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Content-Type': 'application/json'
};
// プリフライトリクエストの処理
if (event.httpMethod === 'OPTIONS') {
return {
statusCode: 200,
headers,
body: ''
};
}
try {
const { httpMethod, queryStringParameters, body, headers: requestHeaders } = event;
const response = {
message: 'Hello from Netlify Functions!',
timestamp: new Date().toISOString(),
method: httpMethod,
query: queryStringParameters || {},
userAgent: requestHeaders['user-agent'] || 'Unknown',
clientIp: event.headers['client-ip'] || 'Unknown'
};
return {
statusCode: 200,
headers,
body: JSON.stringify(response, null, 2)
};
} catch (error) {
console.error('Function error:', error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: 'Internal server error',
message: error.message
})
};
}
};
// netlify/edge-functions/hello-edge.ts - Edge Function (TypeScript)
import { Config } from "https://edge.netlify.com";
export default async (request: Request, context: any): Promise<Response> => {
const url = new URL(request.url);
const name = url.searchParams.get("name") || "World";
const userAgent = request.headers.get("user-agent") || "Unknown";
const country = context.geo?.country?.name || "Unknown";
// レスポンスデータの作成
const responseData = {
message: `Hello, ${name}!`,
timestamp: new Date().toISOString(),
userAgent,
country,
method: request.method,
path: url.pathname,
query: Object.fromEntries(url.searchParams.entries())
};
// JSONレスポンスの作成
const response = new Response(JSON.stringify(responseData, null, 2), {
status: 200,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Cache-Control": "public, max-age=60"
}
});
return response;
};
export const config: Config = {
path: "/api/hello-edge"
};
HTTPAPIとリクエスト処理
// netlify/functions/users.ts - RESTful API実装
import { Handler, HandlerEvent, HandlerContext } from '@netlify/functions';
interface User {
id: string;
name: string;
email: string;
status: 'active' | 'inactive';
createdAt: string;
updatedAt: string;
}
// デモ用のメモリストア(本番ではデータベース使用)
let users: User[] = [
{
id: '1',
name: '田中太郎',
email: '[email protected]',
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}
];
const handler: Handler = async (event: HandlerEvent, context: HandlerContext) => {
const { httpMethod, queryStringParameters, body, path } = event;
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Content-Type': 'application/json'
};
if (httpMethod === 'OPTIONS') {
return { statusCode: 200, headers, body: '' };
}
try {
switch (httpMethod) {
case 'GET':
// ユーザー一覧またはフィルタ検索
const status = queryStringParameters?.status;
const search = queryStringParameters?.search;
let filteredUsers = users;
if (status) {
filteredUsers = filteredUsers.filter(user => user.status === status);
}
if (search) {
filteredUsers = filteredUsers.filter(user =>
user.name.toLowerCase().includes(search.toLowerCase()) ||
user.email.toLowerCase().includes(search.toLowerCase())
);
}
return {
statusCode: 200,
headers,
body: JSON.stringify({
users: filteredUsers,
count: filteredUsers.length,
filters: { status, search }
})
};
case 'POST':
// 新しいユーザーの作成
if (!body) {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: 'Request body is required' })
};
}
const userData = JSON.parse(body);
if (!userData.name || !userData.email) {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: 'Name and email are required' })
};
}
const newUser: User = {
id: Date.now().toString(),
name: userData.name,
email: userData.email,
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
users.push(newUser);
return {
statusCode: 201,
headers,
body: JSON.stringify({
message: 'User created successfully',
user: newUser
})
};
case 'PUT':
// ユーザー情報の更新
if (!body) {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: 'Request body is required' })
};
}
const updateData = JSON.parse(body);
const userId = queryStringParameters?.id;
if (!userId) {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: 'User ID is required' })
};
}
const userIndex = users.findIndex(user => user.id === userId);
if (userIndex === -1) {
return {
statusCode: 404,
headers,
body: JSON.stringify({ error: 'User not found' })
};
}
users[userIndex] = {
...users[userIndex],
...updateData,
updatedAt: new Date().toISOString()
};
return {
statusCode: 200,
headers,
body: JSON.stringify({
message: 'User updated successfully',
user: users[userIndex]
})
};
case 'DELETE':
// ユーザーの削除
const deleteUserId = queryStringParameters?.id;
if (!deleteUserId) {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: 'User ID is required' })
};
}
const deleteIndex = users.findIndex(user => user.id === deleteUserId);
if (deleteIndex === -1) {
return {
statusCode: 404,
headers,
body: JSON.stringify({ error: 'User not found' })
};
}
users.splice(deleteIndex, 1);
return {
statusCode: 200,
headers,
body: JSON.stringify({ message: 'User deleted successfully' })
};
default:
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: 'Method not allowed' })
};
}
} catch (error) {
console.error('API Error:', error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: 'Internal server error',
message: error instanceof Error ? error.message : 'Unknown error'
})
};
}
};
export { handler };
// netlify/edge-functions/api-proxy.ts - Edge Function APIプロキシ
import { Config } from "https://edge.netlify.com";
export default async (request: Request, context: any): Promise<Response> => {
const url = new URL(request.url);
const apiPath = url.pathname.replace('/api/proxy', '');
// レート制限の実装
const clientIp = request.headers.get('CF-Connecting-IP') ||
request.headers.get('X-Forwarded-For') ||
'unknown';
const rateLimitKey = `rate_limit_${clientIp}`;
const requestCount = parseInt(context.cookies.get(rateLimitKey) || '0');
if (requestCount > 100) { // 1分間に100リクエスト制限
return new Response(JSON.stringify({
error: 'Rate limit exceeded',
limit: 100,
window: '1 minute'
}), {
status: 429,
headers: {
'Content-Type': 'application/json',
'Retry-After': '60'
}
});
}
try {
// 外部APIへのプロキシリクエスト
const externalApiUrl = `https://api.external-service.com${apiPath}`;
const proxyRequest = new Request(externalApiUrl, {
method: request.method,
headers: {
'Authorization': `Bearer ${Deno.env.get('EXTERNAL_API_KEY')}`,
'Content-Type': 'application/json',
'User-Agent': 'Netlify-Edge-Proxy/1.0'
},
body: request.method !== 'GET' ? await request.text() : undefined
});
const response = await fetch(proxyRequest);
const data = await response.text();
// レスポンスの変換・フィルタリング
let transformedData = data;
if (response.headers.get('content-type')?.includes('application/json')) {
const jsonData = JSON.parse(data);
// 機密情報の除去
delete jsonData.internal_id;
delete jsonData.api_key;
transformedData = JSON.stringify(jsonData);
}
// レート制限カウンターの更新
const newCount = requestCount + 1;
const responseHeaders = new Headers(response.headers);
responseHeaders.set('Set-Cookie', `${rateLimitKey}=${newCount}; Max-Age=60; HttpOnly`);
responseHeaders.set('X-Rate-Limit-Remaining', (100 - newCount).toString());
return new Response(transformedData, {
status: response.status,
headers: responseHeaders
});
} catch (error) {
console.error('Proxy error:', error);
return new Response(JSON.stringify({
error: 'Proxy request failed',
message: error instanceof Error ? error.message : 'Unknown error'
}), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
};
export const config: Config = {
path: "/api/proxy/*"
};
データベース統合とデータ処理
// netlify/functions/database-users.ts - Supabase統合
import { Handler } from '@netlify/functions';
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = process.env.SUPABASE_URL!;
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
const supabase = createClient(supabaseUrl, supabaseServiceKey);
interface UserRecord {
id?: string;
name: string;
email: string;
status: 'active' | 'inactive';
profile?: {
avatar_url?: string;
bio?: string;
website?: string;
};
created_at?: string;
updated_at?: string;
}
const handler: Handler = async (event) => {
const { httpMethod, queryStringParameters, body } = event;
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Content-Type': 'application/json'
};
if (httpMethod === 'OPTIONS') {
return { statusCode: 200, headers, body: '' };
}
try {
switch (httpMethod) {
case 'GET':
// ユーザー一覧の取得(ページネーション対応)
const page = parseInt(queryStringParameters?.page || '1');
const limit = parseInt(queryStringParameters?.limit || '10');
const search = queryStringParameters?.search;
const status = queryStringParameters?.status;
let query = supabase
.from('users')
.select('*, profiles(*)', { count: 'exact' })
.range((page - 1) * limit, page * limit - 1)
.order('created_at', { ascending: false });
if (search) {
query = query.or(`name.ilike.%${search}%,email.ilike.%${search}%`);
}
if (status) {
query = query.eq('status', status);
}
const { data: users, error, count } = await query;
if (error) {
throw error;
}
return {
statusCode: 200,
headers,
body: JSON.stringify({
users,
pagination: {
total: count,
page,
limit,
pages: Math.ceil((count || 0) / limit)
}
})
};
case 'POST':
// 新しいユーザーの作成
if (!body) {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: 'Request body is required' })
};
}
const userData: UserRecord = JSON.parse(body);
// バリデーション
if (!userData.name || !userData.email) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: 'Name and email are required'
})
};
}
// メールアドレスの重複チェック
const { data: existingUser } = await supabase
.from('users')
.select('id')
.eq('email', userData.email)
.single();
if (existingUser) {
return {
statusCode: 409,
headers,
body: JSON.stringify({
error: 'Email already exists'
})
};
}
// ユーザーの作成
const { data: newUser, error: createError } = await supabase
.from('users')
.insert([{
name: userData.name,
email: userData.email,
status: userData.status || 'active'
}])
.select()
.single();
if (createError) {
throw createError;
}
// プロフィールの作成(オプション)
if (userData.profile && newUser) {
await supabase
.from('profiles')
.insert([{
user_id: newUser.id,
...userData.profile
}]);
}
return {
statusCode: 201,
headers,
body: JSON.stringify({
message: 'User created successfully',
user: newUser
})
};
case 'PUT':
// ユーザー情報の更新
const userId = queryStringParameters?.id;
if (!userId) {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: 'User ID is required' })
};
}
if (!body) {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: 'Request body is required' })
};
}
const updateData: Partial<UserRecord> = JSON.parse(body);
const { data: updatedUser, error: updateError } = await supabase
.from('users')
.update({
name: updateData.name,
email: updateData.email,
status: updateData.status,
updated_at: new Date().toISOString()
})
.eq('id', userId)
.select()
.single();
if (updateError) {
if (updateError.code === 'PGRST116') {
return {
statusCode: 404,
headers,
body: JSON.stringify({ error: 'User not found' })
};
}
throw updateError;
}
return {
statusCode: 200,
headers,
body: JSON.stringify({
message: 'User updated successfully',
user: updatedUser
})
};
case 'DELETE':
// ユーザーの削除
const deleteUserId = queryStringParameters?.id;
if (!deleteUserId) {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: 'User ID is required' })
};
}
const { error: deleteError } = await supabase
.from('users')
.delete()
.eq('id', deleteUserId);
if (deleteError) {
throw deleteError;
}
return {
statusCode: 200,
headers,
body: JSON.stringify({
message: 'User deleted successfully'
})
};
default:
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: 'Method not allowed' })
};
}
} catch (error) {
console.error('Database error:', error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: 'Database operation failed',
message: error instanceof Error ? error.message : 'Unknown error'
})
};
}
};
export { handler };
// netlify/functions/analytics.ts - 分析データ処理
import { Handler } from '@netlify/functions';
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);
interface AnalyticsEvent {
event_type: string;
user_id?: string;
session_id: string;
page_url?: string;
properties: Record<string, any>;
timestamp: string;
user_agent?: string;
ip_address?: string;
}
const handler: Handler = async (event) => {
if (event.httpMethod !== 'POST') {
return {
statusCode: 405,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ error: 'Method not allowed' })
};
}
try {
const events: AnalyticsEvent[] = Array.isArray(JSON.parse(event.body || '[]'))
? JSON.parse(event.body || '[]')
: [JSON.parse(event.body || '{}')];
// イベントの検証と正規化
const validatedEvents = events.map(evt => ({
...evt,
timestamp: evt.timestamp || new Date().toISOString(),
user_agent: event.headers['user-agent'] || evt.user_agent,
ip_address: event.headers['client-ip'] || evt.ip_address,
properties: evt.properties || {}
}));
// バッチでデータベースに挿入
const { error: insertError } = await supabase
.from('analytics_events')
.insert(validatedEvents);
if (insertError) {
throw insertError;
}
// リアルタイム集計の更新
await Promise.all(validatedEvents.map(async (evt) => {
switch (evt.event_type) {
case 'page_view':
await updatePageViewCounters(evt);
break;
case 'user_interaction':
await updateInteractionMetrics(evt);
break;
case 'conversion':
await updateConversionFunnels(evt);
break;
}
}));
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: 'Events processed successfully',
count: validatedEvents.length
})
};
} catch (error) {
console.error('Analytics error:', error);
return {
statusCode: 500,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
error: 'Failed to process analytics events',
message: error instanceof Error ? error.message : 'Unknown error'
})
};
}
};
async function updatePageViewCounters(event: AnalyticsEvent) {
if (!event.page_url) return;
const { error } = await supabase
.from('page_analytics')
.upsert({
page_url: event.page_url,
view_count: 1,
last_viewed: event.timestamp
}, {
onConflict: 'page_url',
ignoreDuplicates: false
});
if (error) {
console.error('Failed to update page view counters:', error);
}
}
async function updateInteractionMetrics(event: AnalyticsEvent) {
if (!event.user_id) return;
const { error } = await supabase
.from('user_interactions')
.upsert({
user_id: event.user_id,
interaction_type: event.properties.interaction_type,
interaction_count: 1,
last_interaction: event.timestamp
}, {
onConflict: 'user_id,interaction_type',
ignoreDuplicates: false
});
if (error) {
console.error('Failed to update interaction metrics:', error);
}
}
async function updateConversionFunnels(event: AnalyticsEvent) {
const { error } = await supabase
.from('conversion_events')
.insert({
user_id: event.user_id,
session_id: event.session_id,
funnel_step: event.properties.step,
conversion_value: event.properties.value || 0,
timestamp: event.timestamp
});
if (error) {
console.error('Failed to track conversion:', error);
}
}
export { handler };
認証とセキュリティ
// netlify/functions/auth-login.ts - JWT認証
import { Handler } from '@netlify/functions';
import { createClient } from '@supabase/supabase-js';
import * as bcrypt from 'bcryptjs';
import * as jwt from 'jsonwebtoken';
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);
const JWT_SECRET = process.env.JWT_SECRET!;
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d';
const handler: Handler = async (event) => {
if (event.httpMethod !== 'POST') {
return {
statusCode: 405,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ error: 'Method not allowed' })
};
}
try {
const { email, password } = JSON.parse(event.body || '{}');
if (!email || !password) {
return {
statusCode: 400,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
error: 'Email and password are required'
})
};
}
// ユーザーの検索
const { data: user, error } = await supabase
.from('users')
.select('id, email, password_hash, name, status, role')
.eq('email', email)
.single();
if (error || !user) {
return {
statusCode: 401,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ error: 'Invalid credentials' })
};
}
// パスワードの検証
const isValidPassword = await bcrypt.compare(password, user.password_hash);
if (!isValidPassword) {
return {
statusCode: 401,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ error: 'Invalid credentials' })
};
}
// アクティブユーザーチェック
if (user.status !== 'active') {
return {
statusCode: 403,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ error: 'Account is not active' })
};
}
// JWTトークンの生成
const token = jwt.sign(
{
userId: user.id,
email: user.email,
role: user.role
},
JWT_SECRET,
{ expiresIn: JWT_EXPIRES_IN }
);
// リフレッシュトークンの生成
const refreshToken = jwt.sign(
{ userId: user.id },
JWT_SECRET + 'refresh',
{ expiresIn: '30d' }
);
// 最終ログイン時刻の更新
await supabase
.from('users')
.update({ last_login_at: new Date().toISOString() })
.eq('id', user.id);
// セキュアなCookieの設定
const cookieOptions = [
`token=${token}; HttpOnly; Secure; SameSite=Strict; Max-Age=604800; Path=/`,
`refreshToken=${refreshToken}; HttpOnly; Secure; SameSite=Strict; Max-Age=2592000; Path=/`
];
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Set-Cookie': cookieOptions
},
body: JSON.stringify({
message: 'Login successful',
user: {
id: user.id,
name: user.name,
email: user.email,
role: user.role
}
})
};
} catch (error) {
console.error('Login error:', error);
return {
statusCode: 500,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
error: 'Authentication failed',
message: error instanceof Error ? error.message : 'Unknown error'
})
};
}
};
export { handler };
// netlify/edge-functions/auth-middleware.ts - 認証ミドルウェア
import { Config } from "https://edge.netlify.com";
import { verify } from "https://deno.land/x/[email protected]/mod.ts";
interface JWTPayload {
userId: string;
email: string;
role: string;
iat: number;
exp: number;
}
export default async (request: Request, context: any): Promise<Response> => {
const url = new URL(request.url);
// 公開ルートのスキップ
const publicPaths = ['/api/auth', '/api/public', '/api/health'];
if (publicPaths.some(path => url.pathname.startsWith(path))) {
return context.next();
}
// 認証が必要なルートのチェック
const protectedPaths = ['/api/admin', '/api/user', '/api/protected'];
const requiresAuth = protectedPaths.some(path => url.pathname.startsWith(path));
if (!requiresAuth) {
return context.next();
}
try {
// トークンの取得
const cookies = parseCookies(request.headers.get('cookie') || '');
const token = cookies.token || request.headers.get('authorization')?.replace('Bearer ', '');
if (!token) {
return new Response(JSON.stringify({
error: 'Authentication required'
}), {
status: 401,
headers: { 'Content-Type': 'application/json' }
});
}
// トークンの検証
const jwtSecret = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(Deno.env.get('JWT_SECRET')),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['verify']
);
const payload = await verify(token, jwtSecret) as JWTPayload;
// ロールベースのアクセス制御
if (url.pathname.startsWith('/api/admin') && payload.role !== 'admin') {
return new Response(JSON.stringify({
error: 'Insufficient permissions'
}), {
status: 403,
headers: { 'Content-Type': 'application/json' }
});
}
// ユーザー情報をヘッダーに追加
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-user-id', payload.userId);
requestHeaders.set('x-user-email', payload.email);
requestHeaders.set('x-user-role', payload.role);
const newRequest = new Request(request.url, {
method: request.method,
headers: requestHeaders,
body: request.body
});
return context.next(newRequest);
} catch (error) {
console.error('Auth middleware error:', error);
return new Response(JSON.stringify({
error: 'Invalid token'
}), {
status: 401,
headers: { 'Content-Type': 'application/json' }
});
}
};
function parseCookies(cookieHeader: string): Record<string, string> {
const cookies: Record<string, string> = {};
cookieHeader.split(';').forEach(cookie => {
const [name, value] = cookie.trim().split('=');
if (name && value) {
cookies[name] = decodeURIComponent(value);
}
});
return cookies;
}
export const config: Config = {
path: "/api/*"
};
イベント駆動アーキテクチャ
// netlify/functions/webhooks-stripe.ts - Stripe Webhook処理
import { Handler } from '@netlify/functions';
import Stripe from 'stripe';
import { createClient } from '@supabase/supabase-js';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2023-10-16',
});
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
const handler: Handler = async (event) => {
if (event.httpMethod !== 'POST') {
return {
statusCode: 405,
body: JSON.stringify({ error: 'Method not allowed' })
};
}
const sig = event.headers['stripe-signature'];
if (!sig) {
return {
statusCode: 400,
body: JSON.stringify({ error: 'Missing stripe signature' })
};
}
let stripeEvent: Stripe.Event;
try {
stripeEvent = stripe.webhooks.constructEvent(
event.body!,
sig,
webhookSecret
);
} catch (err) {
console.error('Webhook signature verification failed:', err);
return {
statusCode: 400,
body: JSON.stringify({ error: 'Invalid signature' })
};
}
try {
switch (stripeEvent.type) {
case 'checkout.session.completed':
await handleCheckoutCompleted(
stripeEvent.data.object as Stripe.Checkout.Session
);
break;
case 'invoice.payment_succeeded':
await handlePaymentSucceeded(
stripeEvent.data.object as Stripe.Invoice
);
break;
case 'customer.subscription.updated':
await handleSubscriptionUpdated(
stripeEvent.data.object as Stripe.Subscription
);
break;
case 'customer.subscription.deleted':
await handleSubscriptionCanceled(
stripeEvent.data.object as Stripe.Subscription
);
break;
default:
console.log(`Unhandled event type: ${stripeEvent.type}`);
}
return {
statusCode: 200,
body: JSON.stringify({ received: true })
};
} catch (error) {
console.error('Webhook processing error:', error);
return {
statusCode: 500,
body: JSON.stringify({
error: 'Webhook processing failed',
message: error instanceof Error ? error.message : 'Unknown error'
})
};
}
};
async function handleCheckoutCompleted(session: Stripe.Checkout.Session) {
console.log('Processing checkout completion:', session.id);
// サブスクリプション情報の更新
if (session.customer && session.subscription) {
const subscription = await stripe.subscriptions.retrieve(
session.subscription as string
);
// ユーザーのサブスクリプション状態を更新
await supabase
.from('user_subscriptions')
.upsert({
customer_id: session.customer as string,
subscription_id: subscription.id,
status: subscription.status,
current_period_start: new Date(subscription.current_period_start * 1000),
current_period_end: new Date(subscription.current_period_end * 1000),
updated_at: new Date().toISOString()
});
// 分析イベントの記録
await fetch(`${process.env.URL}/api/analytics`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
event_type: 'conversion',
properties: {
step: 'subscription_created',
value: session.amount_total! / 100,
currency: session.currency,
subscription_id: session.subscription
},
session_id: session.id,
timestamp: new Date().toISOString()
})
});
}
}
async function handlePaymentSucceeded(invoice: Stripe.Invoice) {
console.log('Processing successful payment:', invoice.id);
// 支払い記録の保存
await supabase
.from('payments')
.insert({
invoice_id: invoice.id,
customer_id: invoice.customer as string,
amount: invoice.amount_paid,
currency: invoice.currency,
status: 'succeeded',
paid_at: new Date(invoice.status_transitions.paid_at! * 1000),
created_at: new Date().toISOString()
});
}
async function handleSubscriptionUpdated(subscription: Stripe.Subscription) {
console.log('Processing subscription update:', subscription.id);
// サブスクリプション詳細の更新
await supabase
.from('user_subscriptions')
.update({
status: subscription.status,
current_period_start: new Date(subscription.current_period_start * 1000),
current_period_end: new Date(subscription.current_period_end * 1000),
cancel_at_period_end: subscription.cancel_at_period_end,
updated_at: new Date().toISOString()
})
.eq('subscription_id', subscription.id);
}
async function handleSubscriptionCanceled(subscription: Stripe.Subscription) {
console.log('Processing subscription cancellation:', subscription.id);
// サブスクリプション状態の更新
await supabase
.from('user_subscriptions')
.update({
status: 'canceled',
canceled_at: new Date().toISOString(),
updated_at: new Date().toISOString()
})
.eq('subscription_id', subscription.id);
}
export { handler };
監視とパフォーマンス最適化
// netlify/functions/health.ts - ヘルスチェックエンドポイント
import { Handler } from '@netlify/functions';
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);
interface HealthStatus {
status: 'healthy' | 'unhealthy';
timestamp: string;
services: {
database: 'healthy' | 'unhealthy';
external_apis: 'healthy' | 'unhealthy';
};
metrics: {
response_time: number;
memory_usage: number;
function_region: string;
};
}
const handler: Handler = async (event, context) => {
const startTime = Date.now();
try {
// データベース接続確認
const dbHealthy = await checkDatabaseHealth();
// 外部API確認
const externalApisHealthy = await checkExternalApis();
// メトリクス計算
const responseTime = Date.now() - startTime;
const memoryUsage = process.memoryUsage().heapUsed / 1024 / 1024; // MB
const allHealthy = dbHealthy && externalApisHealthy;
const healthStatus: HealthStatus = {
status: allHealthy ? 'healthy' : 'unhealthy',
timestamp: new Date().toISOString(),
services: {
database: dbHealthy ? 'healthy' : 'unhealthy',
external_apis: externalApisHealthy ? 'healthy' : 'unhealthy'
},
metrics: {
response_time: responseTime,
memory_usage: Math.round(memoryUsage * 100) / 100,
function_region: process.env.AWS_REGION || 'unknown'
}
};
const statusCode = allHealthy ? 200 : 503;
return {
statusCode,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(healthStatus)
};
} catch (error) {
console.error('Health check error:', error);
const errorStatus: HealthStatus = {
status: 'unhealthy',
timestamp: new Date().toISOString(),
services: {
database: 'unhealthy',
external_apis: 'unhealthy'
},
metrics: {
response_time: Date.now() - startTime,
memory_usage: 0,
function_region: process.env.AWS_REGION || 'unknown'
}
};
return {
statusCode: 503,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errorStatus)
};
}
};
async function checkDatabaseHealth(): Promise<boolean> {
try {
const { data, error } = await supabase
.from('health_check')
.select('id')
.limit(1);
return !error;
} catch (error) {
console.error('Database health check failed:', error);
return false;
}
}
async function checkExternalApis(): Promise<boolean> {
try {
const checks = await Promise.allSettled([
fetch('https://api.stripe.com/v1', {
method: 'HEAD',
headers: { 'Authorization': `Bearer ${process.env.STRIPE_SECRET_KEY}` }
}),
// 他の外部APIチェックを必要に応じて追加
]);
return checks.every(result =>
result.status === 'fulfilled' &&
result.value.ok
);
} catch (error) {
console.error('External API health check failed:', error);
return false;
}
}
export { handler };
# netlify.toml - プロジェクト設定ファイル
[build]
publish = "dist"
command = "npm run build"
functions = "netlify/functions"
edge_functions = "netlify/edge-functions"
[build.environment]
NODE_VERSION = "18"
[[redirects]]
from = "/api/*"
to = "/.netlify/functions/:splat"
status = 200
[[edge_functions]]
function = "auth-middleware"
path = "/api/*"
[[edge_functions]]
function = "hello-edge"
path = "/api/hello-edge"
[[edge_functions]]
function = "api-proxy"
path = "/api/proxy/*"
[functions]
node_bundler = "nft"
[functions."auth-login"]
external_node_modules = ["bcryptjs", "jsonwebtoken"]
[functions."database-users"]
external_node_modules = ["@supabase/supabase-js"]
[functions."webhooks-stripe"]
external_node_modules = ["stripe"]
[dev]
command = "npm run dev"
port = 3000
targetPort = 8888
framework = "#static"
[context.production.environment]
NODE_ENV = "production"
[context.deploy-preview.environment]
NODE_ENV = "staging"
[context.branch-deploy.environment]
NODE_ENV = "development"
Netlify Functionsは、JAMstackアーキテクチャに最適化されたサーバーレスプラットフォームとして、静的サイトホスティングと統合されたフルスタック開発を実現し、Git連携による効率的な開発ワークフローと高速なEdge Functions により、モダンWebアプリケーション開発の新しいスタンダードを提供しています。