Vercel Functions
プラットフォーム
Vercel Functions
概要
Vercel Functionsは、Vercelが提供するサーバーレス関数実行環境として、フロントエンドアプリケーションとシームレスに統合し、API エンドポイント、ミドルウェア、バックエンド処理を簡単に構築できる革新的なプラットフォームです。Next.js、React、Vue.js、Svelte等のモダンWebフレームワークとの深い統合により、フルスタック開発の効率性を大幅に向上させています。Edge RuntimeとNode.js Runtimeの両方をサポートし、TypeScript、ゼロ設定、ストリーミング対応などの開発者体験を重視した機能により、2025年においてJAMstackアーキテクチャの中核的存在として注目を集めています。
詳細
Vercel Functions 2025年版は、モダンWeb開発のためのサーバーレスコンピューティングプラットフォームとして、特にNext.js、React アプリケーションでの利用が急拡大しています。Edge Runtimeによる全世界14ヶ所のエッジロケーションでの高速実行、TypeScriptネイティブサポート、ゼロ設定によるシームレスなデプロイメント、リアルタイムストリーミング機能など、フロントエンド開発者にとって理想的な環境を提供。Vercel AI SDKとの統合によりAIアプリケーション開発も強力にサポートし、Gitベースのワークフロー、プレビューデプロイメント、段階的ロールアウトなど、チーム開発を支援する豊富な機能を備えています。
主な特徴
- Edge Runtime: 全世界のエッジでの高速実行
- Node.js サポート: 従来のNode.jsランタイム完全対応
- TypeScript ネイティブ: 型安全な開発環境
- ゼロ設定: 設定不要のシームレスデプロイ
- ストリーミング: リアルタイムデータ配信対応
- Next.js 統合: フルスタック開発の最適化
メリット・デメリット
メリット
- Next.jsエコシステムとの完璧な統合による開発効率の向上
- Edge Runtimeによる全世界的な低遅延実行環境
- TypeScriptネイティブサポートによる型安全な開発体験
- Gitベースの自動デプロイとプレビュー機能
- Vercel AI SDKとの統合によるAIアプリケーション開発支援
- ゼロ設定でのシームレスなスケーリングと運用
デメリット
- Vercelプラットフォームへの強い依存とベンダーロックイン
- 他のクラウドプロバイダーと比較して限定的なインフラストラクチャー制御
- 大量同時実行時のコスト予測が困難な課金体系
- Edge Runtime環境での Node.js API制限
- エンタープライズ機能や高度なカスタマイズ性の不足
- 複雑なバックエンドロジックには機能制約がある
参考ページ
書き方の例
セットアップと関数作成
# Vercel CLIのインストール
npm install -g vercel
# プロジェクトの初期化とデプロイ
npx create-next-app@latest my-vercel-app
cd my-vercel-app
vercel
# ローカル開発サーバーの起動
vercel dev
// api/hello.js - 基本的なAPI関数
export default function handler(req, res) {
console.log('Method:', req.method);
console.log('Query:', req.query);
console.log('Headers:', req.headers);
// CORS対応
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') {
res.status(200).end();
return;
}
try {
const response = {
message: 'Hello from Vercel Functions!',
timestamp: new Date().toISOString(),
method: req.method,
query: req.query,
userAgent: req.headers['user-agent'] || 'Unknown'
};
res.status(200).json(response);
} catch (error) {
console.error('Function error:', error);
res.status(500).json({
error: 'Internal server error',
message: error.message
});
}
}
// api/users/[id].ts - TypeScript動的ルーティング
import { NextApiRequest, NextApiResponse } from 'next';
interface User {
id: string;
name: string;
email: string;
createdAt: string;
}
interface ApiResponse {
success: boolean;
data?: User;
error?: string;
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<ApiResponse>
) {
const { id } = req.query;
if (!id || typeof id !== 'string') {
return res.status(400).json({
success: false,
error: 'Invalid user ID'
});
}
try {
switch (req.method) {
case 'GET':
const user = await getUserById(id);
if (!user) {
return res.status(404).json({
success: false,
error: 'User not found'
});
}
return res.status(200).json({
success: true,
data: user
});
case 'PUT':
const updatedUser = await updateUser(id, req.body);
return res.status(200).json({
success: true,
data: updatedUser
});
case 'DELETE':
await deleteUser(id);
return res.status(204).end();
default:
res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
return res.status(405).json({
success: false,
error: `Method ${req.method} not allowed`
});
}
} catch (error) {
console.error('API error:', error);
return res.status(500).json({
success: false,
error: 'Internal server error'
});
}
}
async function getUserById(id: string): Promise<User | null> {
// データベースからユーザー取得のシミュレーション
return {
id,
name: `User ${id}`,
email: `user${id}@example.com`,
createdAt: new Date().toISOString()
};
}
async function updateUser(id: string, data: Partial<User>): Promise<User> {
// ユーザー更新のシミュレーション
return {
id,
name: data.name || `User ${id}`,
email: data.email || `user${id}@example.com`,
createdAt: new Date().toISOString()
};
}
async function deleteUser(id: string): Promise<void> {
// ユーザー削除のシミュレーション
console.log(`User ${id} deleted`);
}
HTTP APIとリクエスト処理
// api/posts/index.ts - RESTful API エンドポイント
import { NextApiRequest, NextApiResponse } from 'next';
interface Post {
id: string;
title: string;
content: string;
author: string;
publishedAt: string;
tags: string[];
}
interface CreatePostRequest {
title: string;
content: string;
author: string;
tags?: string[];
}
interface PostsApiResponse {
success: boolean;
data?: Post | Post[];
error?: string;
pagination?: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<PostsApiResponse>
) {
try {
switch (req.method) {
case 'GET':
return await handleGetPosts(req, res);
case 'POST':
return await handleCreatePost(req, res);
default:
res.setHeader('Allow', ['GET', 'POST']);
return res.status(405).json({
success: false,
error: `Method ${req.method} not allowed`
});
}
} catch (error) {
console.error('Posts API error:', error);
return res.status(500).json({
success: false,
error: 'Internal server error'
});
}
}
async function handleGetPosts(
req: NextApiRequest,
res: NextApiResponse<PostsApiResponse>
) {
const page = parseInt(req.query.page as string) || 1;
const limit = parseInt(req.query.limit as string) || 10;
const search = req.query.search as string;
const tag = req.query.tag as string;
// クエリパラメータバリデーション
if (page < 1 || limit < 1 || limit > 100) {
return res.status(400).json({
success: false,
error: 'Invalid pagination parameters'
});
}
try {
const { posts, total } = await fetchPosts({
page,
limit,
search,
tag
});
const totalPages = Math.ceil(total / limit);
return res.status(200).json({
success: true,
data: posts,
pagination: {
page,
limit,
total,
totalPages
}
});
} catch (error) {
console.error('Fetch posts error:', error);
return res.status(500).json({
success: false,
error: 'Failed to fetch posts'
});
}
}
async function handleCreatePost(
req: NextApiRequest,
res: NextApiResponse<PostsApiResponse>
) {
const { title, content, author, tags = [] }: CreatePostRequest = req.body;
// リクエストボディバリデーション
if (!title || !content || !author) {
return res.status(400).json({
success: false,
error: 'Title, content, and author are required'
});
}
if (title.length > 200 || content.length > 10000) {
return res.status(400).json({
success: false,
error: 'Title or content too long'
});
}
try {
const newPost = await createPost({
title: title.trim(),
content: content.trim(),
author: author.trim(),
tags: tags.map(tag => tag.trim()).filter(Boolean)
});
return res.status(201).json({
success: true,
data: newPost
});
} catch (error) {
console.error('Create post error:', error);
return res.status(500).json({
success: false,
error: 'Failed to create post'
});
}
}
async function fetchPosts({ page, limit, search, tag }: {
page: number;
limit: number;
search?: string;
tag?: string;
}): Promise<{ posts: Post[]; total: number }> {
// データベースからの投稿取得をシミュレーション
const mockPosts: Post[] = Array.from({ length: limit }, (_, i) => ({
id: `post-${page}-${i + 1}`,
title: `Sample Post ${page}-${i + 1}`,
content: `This is the content of post ${page}-${i + 1}`,
author: `Author ${i + 1}`,
publishedAt: new Date().toISOString(),
tags: ['technology', 'web-development']
}));
// 検索とタグフィルタリングをシミュレーション
let filteredPosts = mockPosts;
if (search) {
filteredPosts = filteredPosts.filter(post =>
post.title.toLowerCase().includes(search.toLowerCase()) ||
post.content.toLowerCase().includes(search.toLowerCase())
);
}
if (tag) {
filteredPosts = filteredPosts.filter(post =>
post.tags.includes(tag)
);
}
return {
posts: filteredPosts,
total: 1000 // 総数をシミュレーション
};
}
async function createPost(postData: CreatePostRequest): Promise<Post> {
// 新規投稿作成をシミュレーション
const newPost: Post = {
id: `post-${Date.now()}`,
title: postData.title,
content: postData.content,
author: postData.author,
publishedAt: new Date().toISOString(),
tags: postData.tags || []
};
// データベース保存のシミュレーション
console.log('Creating post:', newPost);
return newPost;
}
データベース統合とデータ処理
// api/database/users.ts - データベース統合例
import { NextApiRequest, NextApiResponse } from 'next';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
interface DatabaseResponse {
success: boolean;
data?: any;
error?: string;
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<DatabaseResponse>
) {
try {
switch (req.method) {
case 'GET':
return await handleGetUsers(req, res);
case 'POST':
return await handleCreateUser(req, res);
default:
res.setHeader('Allow', ['GET', 'POST']);
return res.status(405).json({
success: false,
error: `Method ${req.method} not allowed`
});
}
} catch (error) {
console.error('Database API error:', error);
return res.status(500).json({
success: false,
error: 'Database operation failed'
});
} finally {
await prisma.$disconnect();
}
}
async function handleGetUsers(
req: NextApiRequest,
res: NextApiResponse<DatabaseResponse>
) {
const { page = '1', limit = '10', search = '' } = req.query;
const pageNum = parseInt(page as string);
const limitNum = parseInt(limit as string);
const skip = (pageNum - 1) * limitNum;
try {
const whereClause = search ? {
OR: [
{ name: { contains: search as string, mode: 'insensitive' } },
{ email: { contains: search as string, mode: 'insensitive' } }
]
} : {};
const [users, totalCount] = await Promise.all([
prisma.user.findMany({
where: whereClause,
skip,
take: limitNum,
select: {
id: true,
name: true,
email: true,
createdAt: true,
profile: {
select: {
bio: true,
avatar: true
}
},
_count: {
select: {
posts: true,
comments: true
}
}
}
}),
prisma.user.count({ where: whereClause })
]);
return res.status(200).json({
success: true,
data: {
users,
pagination: {
page: pageNum,
limit: limitNum,
total: totalCount,
totalPages: Math.ceil(totalCount / limitNum)
}
}
});
} catch (error) {
console.error('Get users error:', error);
return res.status(500).json({
success: false,
error: 'Failed to fetch users'
});
}
}
async function handleCreateUser(
req: NextApiRequest,
res: NextApiResponse<DatabaseResponse>
) {
const { name, email, bio, avatar } = req.body;
// バリデーション
if (!name || !email) {
return res.status(400).json({
success: false,
error: 'Name and email are required'
});
}
// メールアドレスの形式チェック
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return res.status(400).json({
success: false,
error: 'Invalid email format'
});
}
try {
// トランザクションでユーザーとプロフィールを作成
const newUser = await prisma.$transaction(async (tx) => {
// メール重複チェック
const existingUser = await tx.user.findUnique({
where: { email }
});
if (existingUser) {
throw new Error('Email already exists');
}
// ユーザー作成
const user = await tx.user.create({
data: {
name,
email,
profile: bio || avatar ? {
create: {
bio: bio || null,
avatar: avatar || null
}
} : undefined
},
include: {
profile: true
}
});
return user;
});
return res.status(201).json({
success: true,
data: newUser
});
} catch (error) {
console.error('Create user error:', error);
if (error.message === 'Email already exists') {
return res.status(409).json({
success: false,
error: 'Email already exists'
});
}
return res.status(500).json({
success: false,
error: 'Failed to create user'
});
}
}
認証とセキュリティ
// api/auth/middleware.ts - 認証ミドルウェア
import { NextApiRequest, NextApiResponse } from 'next';
import jwt from 'jsonwebtoken';
interface AuthenticatedRequest extends NextApiRequest {
user?: {
id: string;
email: string;
role: string;
};
}
export interface AuthMiddlewareOptions {
requiredRole?: string;
requireAuth?: boolean;
}
export function withAuth(
handler: (req: AuthenticatedRequest, res: NextApiResponse) => Promise<void>,
options: AuthMiddlewareOptions = {}
) {
return async (req: AuthenticatedRequest, res: NextApiResponse) => {
try {
// CORS ヘッダーの設定
res.setHeader('Access-Control-Allow-Origin', process.env.ALLOWED_ORIGINS || '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') {
return res.status(200).end();
}
// 認証が必要でない場合はそのまま実行
if (!options.requireAuth) {
return await handler(req, res);
}
// トークンの取得
const token = extractToken(req);
if (!token) {
return res.status(401).json({
success: false,
error: 'Authorization token required'
});
}
// トークンの検証
const decoded = await verifyToken(token);
req.user = decoded;
// ロールベースの認可
if (options.requiredRole && decoded.role !== options.requiredRole) {
return res.status(403).json({
success: false,
error: 'Insufficient permissions'
});
}
// レート制限の実装
const rateLimitResult = await checkRateLimit(req);
if (!rateLimitResult.allowed) {
return res.status(429).json({
success: false,
error: 'Rate limit exceeded',
retryAfter: rateLimitResult.retryAfter
});
}
return await handler(req, res);
} catch (error) {
console.error('Auth middleware error:', error);
if (error.name === 'JsonWebTokenError') {
return res.status(401).json({
success: false,
error: 'Invalid token'
});
}
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
success: false,
error: 'Token expired'
});
}
return res.status(500).json({
success: false,
error: 'Authentication failed'
});
}
};
}
function extractToken(req: NextApiRequest): string | null {
const authHeader = req.headers.authorization;
if (!authHeader) return null;
const parts = authHeader.split(' ');
if (parts.length !== 2 || parts[0] !== 'Bearer') return null;
return parts[1];
}
async function verifyToken(token: string): Promise<{
id: string;
email: string;
role: string;
}> {
const secret = process.env.JWT_SECRET;
if (!secret) {
throw new Error('JWT_SECRET not configured');
}
const decoded = jwt.verify(token, secret) as any;
return {
id: decoded.sub,
email: decoded.email,
role: decoded.role || 'user'
};
}
interface RateLimitResult {
allowed: boolean;
retryAfter?: number;
}
async function checkRateLimit(req: NextApiRequest): Promise<RateLimitResult> {
// IPアドレスベースのレート制限(簡易実装)
const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
const key = `rate_limit:${ip}`;
// Redis或いはメモリキャッシュでの実装をシミュレート
// 実際の実装では、Vercel KVやUpstash Redisを使用
return { allowed: true }; // 簡易実装のため常に許可
}
// api/auth/login.ts - ログイン認証
import { NextApiRequest, NextApiResponse } from 'next';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
interface LoginRequest {
email: string;
password: string;
}
interface LoginResponse {
success: boolean;
data?: {
token: string;
user: {
id: string;
name: string;
email: string;
role: string;
};
};
error?: string;
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<LoginResponse>
) {
if (req.method !== 'POST') {
res.setHeader('Allow', ['POST']);
return res.status(405).json({
success: false,
error: 'Method not allowed'
});
}
try {
const { email, password }: LoginRequest = req.body;
// バリデーション
if (!email || !password) {
return res.status(400).json({
success: false,
error: 'Email and password are required'
});
}
// ユーザー検索
const user = await prisma.user.findUnique({
where: { email },
include: {
profile: true
}
});
if (!user) {
return res.status(401).json({
success: false,
error: 'Invalid credentials'
});
}
// パスワード検証
const isValidPassword = await bcrypt.compare(password, user.passwordHash);
if (!isValidPassword) {
return res.status(401).json({
success: false,
error: 'Invalid credentials'
});
}
// JWTトークン生成
const token = jwt.sign(
{
sub: user.id,
email: user.email,
role: user.role,
iat: Math.floor(Date.now() / 1000)
},
process.env.JWT_SECRET!,
{ expiresIn: '24h' }
);
// ログイン記録
await prisma.loginLog.create({
data: {
userId: user.id,
ipAddress: req.headers['x-forwarded-for'] as string || 'unknown',
userAgent: req.headers['user-agent'] || 'unknown',
loginAt: new Date()
}
});
return res.status(200).json({
success: true,
data: {
token,
user: {
id: user.id,
name: user.name,
email: user.email,
role: user.role
}
}
});
} catch (error) {
console.error('Login error:', error);
return res.status(500).json({
success: false,
error: 'Login failed'
});
} finally {
await prisma.$disconnect();
}
}
イベント駆動アーキテクチャ
// api/webhook/stripe.ts - Stripeウェブフック処理
import { NextApiRequest, NextApiResponse } from 'next';
import Stripe from 'stripe';
import { buffer } from 'micro';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2023-10-16',
});
// Next.jsのbody parserを無効化
export const config = {
api: {
bodyParser: false,
},
};
interface WebhookResponse {
success: boolean;
message?: string;
error?: string;
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<WebhookResponse>
) {
if (req.method !== 'POST') {
res.setHeader('Allow', ['POST']);
return res.status(405).json({
success: false,
error: 'Method not allowed'
});
}
try {
// リクエストボディを取得
const buf = await buffer(req);
const signature = req.headers['stripe-signature'] as string;
if (!signature) {
return res.status(400).json({
success: false,
error: 'Missing Stripe signature'
});
}
// Stripeイベントの検証
const event = stripe.webhooks.constructEvent(
buf,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
console.log('Stripe event received:', event.type);
// イベントタイプに応じた処理
switch (event.type) {
case 'payment_intent.succeeded':
await handlePaymentSucceeded(event.data.object);
break;
case 'payment_intent.payment_failed':
await handlePaymentFailed(event.data.object);
break;
case 'customer.subscription.created':
await handleSubscriptionCreated(event.data.object);
break;
case 'customer.subscription.deleted':
await handleSubscriptionDeleted(event.data.object);
break;
case 'invoice.payment_succeeded':
await handleInvoicePaymentSucceeded(event.data.object);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
return res.status(200).json({
success: true,
message: 'Webhook processed successfully'
});
} catch (error) {
console.error('Stripe webhook error:', error);
if (error instanceof Error && error.message.includes('signature')) {
return res.status(400).json({
success: false,
error: 'Invalid signature'
});
}
return res.status(500).json({
success: false,
error: 'Webhook processing failed'
});
}
}
async function handlePaymentSucceeded(paymentIntent: any) {
console.log('Payment succeeded:', paymentIntent.id);
try {
// 注文状態の更新
await updateOrderStatus(paymentIntent.metadata.orderId, 'paid');
// 顧客への確認メール送信
await sendPaymentConfirmationEmail(
paymentIntent.metadata.customerEmail,
paymentIntent.amount,
paymentIntent.currency
);
// 在庫の更新
await updateInventory(paymentIntent.metadata.orderId);
// 売上分析データの記録
await recordSalesAnalytics(paymentIntent);
} catch (error) {
console.error('Payment succeeded handler error:', error);
}
}
async function handlePaymentFailed(paymentIntent: any) {
console.log('Payment failed:', paymentIntent.id);
try {
// 注文状態の更新
await updateOrderStatus(paymentIntent.metadata.orderId, 'payment_failed');
// 顧客への失敗通知
await sendPaymentFailedEmail(
paymentIntent.metadata.customerEmail,
paymentIntent.last_payment_error?.message || 'Payment failed'
);
// 再試行のスケジューリング
await schedulePaymentRetry(paymentIntent.metadata.orderId);
} catch (error) {
console.error('Payment failed handler error:', error);
}
}
async function handleSubscriptionCreated(subscription: any) {
console.log('Subscription created:', subscription.id);
try {
// ユーザーの権限アップグレード
await upgradeUserPermissions(subscription.customer, subscription.items.data[0].price.id);
// ウェルカムメールの送信
await sendWelcomeEmail(subscription.customer);
// 課金分析データの記録
await recordSubscriptionAnalytics(subscription);
} catch (error) {
console.error('Subscription created handler error:', error);
}
}
async function updateOrderStatus(orderId: string, status: string) {
// データベースの注文状態更新をシミュレート
console.log(`Updating order ${orderId} status to ${status}`);
}
async function sendPaymentConfirmationEmail(email: string, amount: number, currency: string) {
// メール送信をシミュレート
console.log(`Sending confirmation email to ${email} for ${amount} ${currency}`);
}
async function updateInventory(orderId: string) {
// 在庫更新をシミュレート
console.log(`Updating inventory for order ${orderId}`);
}
async function recordSalesAnalytics(paymentIntent: any) {
// 売上分析データ記録をシミュレート
console.log('Recording sales analytics:', paymentIntent.id);
}
監視とパフォーマンス最適化
// api/monitoring/health.ts - ヘルスチェックとモニタリング
import { NextApiRequest, NextApiResponse } from 'next';
interface HealthCheckResponse {
status: 'healthy' | 'unhealthy';
timestamp: string;
uptime: number;
version: string;
checks: {
database: { status: 'up' | 'down'; responseTime?: number };
redis: { status: 'up' | 'down'; responseTime?: number };
external_api: { status: 'up' | 'down'; responseTime?: number };
};
memory: {
used: number;
total: number;
percentage: number;
};
performance: {
averageResponseTime: number;
requestCount: number;
errorRate: number;
};
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<HealthCheckResponse>
) {
const startTime = Date.now();
try {
// 各種システムの健全性チェック
const [databaseHealth, redisHealth, externalApiHealth] = await Promise.allSettled([
checkDatabaseHealth(),
checkRedisHealth(),
checkExternalApiHealth()
]);
// メモリ使用量の取得
const memoryUsage = process.memoryUsage();
const memoryTotal = memoryUsage.heapTotal;
const memoryUsed = memoryUsage.heapUsed;
const memoryPercentage = (memoryUsed / memoryTotal) * 100;
// パフォーマンス情報の取得
const performanceData = await getPerformanceMetrics();
// 全体的な健全性の判定
const allHealthy = [databaseHealth, redisHealth, externalApiHealth]
.every(result => result.status === 'fulfilled' && result.value.status === 'up');
const healthResponse: HealthCheckResponse = {
status: allHealthy ? 'healthy' : 'unhealthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
version: process.env.npm_package_version || '1.0.0',
checks: {
database: databaseHealth.status === 'fulfilled' ? databaseHealth.value : { status: 'down' },
redis: redisHealth.status === 'fulfilled' ? redisHealth.value : { status: 'down' },
external_api: externalApiHealth.status === 'fulfilled' ? externalApiHealth.value : { status: 'down' }
},
memory: {
used: Math.round(memoryUsed / 1024 / 1024), // MB
total: Math.round(memoryTotal / 1024 / 1024), // MB
percentage: Math.round(memoryPercentage * 100) / 100
},
performance: performanceData
};
// 応答時間の記録
const responseTime = Date.now() - startTime;
await recordMetric('health_check_response_time', responseTime);
const statusCode = allHealthy ? 200 : 503;
return res.status(statusCode).json(healthResponse);
} catch (error) {
console.error('Health check error:', error);
return res.status(503).json({
status: 'unhealthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
version: process.env.npm_package_version || '1.0.0',
checks: {
database: { status: 'down' },
redis: { status: 'down' },
external_api: { status: 'down' }
},
memory: {
used: 0,
total: 0,
percentage: 0
},
performance: {
averageResponseTime: 0,
requestCount: 0,
errorRate: 100
}
});
}
}
async function checkDatabaseHealth(): Promise<{ status: 'up' | 'down'; responseTime?: number }> {
const startTime = Date.now();
try {
// データベース接続テスト(例:簡単なクエリ実行)
// await prisma.$queryRaw`SELECT 1`;
const responseTime = Date.now() - startTime;
return { status: 'up', responseTime };
} catch (error) {
console.error('Database health check failed:', error);
return { status: 'down' };
}
}
async function checkRedisHealth(): Promise<{ status: 'up' | 'down'; responseTime?: number }> {
const startTime = Date.now();
try {
// Redis接続テスト
// await redis.ping();
const responseTime = Date.now() - startTime;
return { status: 'up', responseTime };
} catch (error) {
console.error('Redis health check failed:', error);
return { status: 'down' };
}
}
async function checkExternalApiHealth(): Promise<{ status: 'up' | 'down'; responseTime?: number }> {
const startTime = Date.now();
try {
// 外部API接続テスト
const response = await fetch('https://api.example.com/health', {
method: 'GET',
timeout: 5000
});
const responseTime = Date.now() - startTime;
if (response.ok) {
return { status: 'up', responseTime };
} else {
return { status: 'down' };
}
} catch (error) {
console.error('External API health check failed:', error);
return { status: 'down' };
}
}
async function getPerformanceMetrics(): Promise<{
averageResponseTime: number;
requestCount: number;
errorRate: number;
}> {
// パフォーマンスメトリクスの取得をシミュレート
// 実際の実装では、メトリクス収集システムから取得
return {
averageResponseTime: 150, // ms
requestCount: 1250,
errorRate: 2.5 // %
};
}
async function recordMetric(metricName: string, value: number): Promise<void> {
// メトリクス記録をシミュレート
// 実際の実装では、CloudWatch、DataDog、または Vercel Analytics に送信
console.log(`Metric recorded: ${metricName} = ${value}`);
}
デプロイメントと本番運用
// vercel.json - Vercel設定ファイル
{
"functions": {
"api/**/*.ts": {
"runtime": "@vercel/[email protected]"
},
"api/edge/**/*.ts": {
"runtime": "edge"
}
},
"regions": ["iad1", "hnd1", "lhr1"],
"env": {
"DATABASE_URL": "@database-url",
"JWT_SECRET": "@jwt-secret",
"STRIPE_SECRET_KEY": "@stripe-secret",
"REDIS_URL": "@redis-url"
},
"build": {
"env": {
"NODE_ENV": "production"
}
},
"headers": [
{
"source": "/api/(.*)",
"headers": [
{
"key": "Access-Control-Allow-Origin",
"value": "*"
},
{
"key": "Access-Control-Allow-Methods",
"value": "GET, POST, PUT, DELETE, OPTIONS"
},
{
"key": "Access-Control-Allow-Headers",
"value": "Content-Type, Authorization"
}
]
}
],
"rewrites": [
{
"source": "/api/v1/(.*)",
"destination": "/api/$1"
}
]
}
#!/bin/bash
# deploy.sh - デプロイメントスクリプト
set -e
# 環境変数の設定
ENVIRONMENT=${1:-production}
PROJECT_NAME="my-vercel-app"
echo "Deploying to environment: ${ENVIRONMENT}"
# プロジェクトのビルドテスト
echo "Building project..."
npm run build
# 本番環境へのデプロイ
if [ "$ENVIRONMENT" == "production" ]; then
echo "Deploying to production..."
vercel --prod
elif [ "$ENVIRONMENT" == "preview" ]; then
echo "Deploying to preview..."
vercel
else
echo "Invalid environment: ${ENVIRONMENT}"
exit 1
fi
# デプロイ後のヘルスチェック
echo "Performing health check..."
if [ "$ENVIRONMENT" == "production" ]; then
HEALTH_URL="https://${PROJECT_NAME}.vercel.app/api/monitoring/health"
else
# プレビューURLを取得(簡略化)
HEALTH_URL="https://preview-url/api/monitoring/health"
fi
# ヘルスチェック実行
curl -f ${HEALTH_URL} || {
echo "Health check failed!"
exit 1
}
echo "Deployment completed successfully!"
#!/bin/bash
# monitor.sh - 監視スクリプト
FUNCTION_URL="https://my-vercel-app.vercel.app"
echo "Monitoring Vercel Functions..."
# APIエンドポイントの監視
check_endpoint() {
local endpoint=$1
local expected_status=$2
echo "Checking ${endpoint}..."
response=$(curl -s -w "HTTPSTATUS:%{http_code};TIME:%{time_total}" ${FUNCTION_URL}${endpoint})
http_status=$(echo $response | grep -o "HTTPSTATUS:[0-9]*" | cut -d: -f2)
response_time=$(echo $response | grep -o "TIME:[0-9.]*" | cut -d: -f2)
if [ "$http_status" == "$expected_status" ]; then
echo "✓ ${endpoint} - Status: ${http_status}, Time: ${response_time}s"
else
echo "✗ ${endpoint} - Expected: ${expected_status}, Got: ${http_status}"
fi
}
# 各エンドポイントの確認
check_endpoint "/api/monitoring/health" "200"
check_endpoint "/api/hello" "200"
check_endpoint "/api/users" "200"
# ログの確認(Vercel CLI使用)
echo "Fetching recent logs..."
vercel logs --output raw | tail -20
echo "Monitoring complete!"