Netlify Functions

サーバーレスNetlifyEdge FunctionsJAMstackDenoNode.js

プラットフォーム

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アプリケーション開発の新しいスタンダードを提供しています。