nextjs-api-routes について
nextjs-api-routesは、Next.js 15 App RouterでAPIルートを構築するためのパターン集です。リクエスト処理や認証などを実装する開発者向けです。
主な機能
- 01GET、POST、PUT、DELETEのパターン
- 02入力検証とエラー処理
- 03認証とCRONシークレットの確認
- 04ランタイムとタイムアウトの設定
- 05CORS、ヘッダー、レスポンス形式
活用シーン
- 01App Router APIエンドポイントの作成
- 02保護されたルートやCRONルートの追加
- 03ページ分割・一括レスポンスの処理
- 04ルートのランタイムとタイムアウトの選択
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ venture-formations/ aiprodaily --skill nextjs-api-routesインストール後、Claude Code で/nextjs-api-routesを入力して読み込まれたか確認します。
詳細
Next.js API ルート - パターンライブラリ
目的
AIProDaily プラットフォーム向けに Next.js 15 で API ルートを構築するための包括的なガイドです。リクエスト処理、エラー管理、認証、パフォーマンス最適化について説明します。
使用する場合
以下の場合に自動的に有効になります:
app/api/**/*.tsに新しい API ルートを作成する- NextRequest/NextResponse を扱う
- ルートのタイムアウトを設定する
- 認証を処理する
- API リクエストを処理する
- サーバーサイドのエンドポイントを構築する
クイックスタート:API ルートのテンプレート
標準的な POST ルート
// app/api/[feature]/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase'
export async function POST(request: NextRequest) {
try {
// 1. Parse request body
const body = await request.json()
// 2. Validate required fields
if (!body.campaignId) {
return NextResponse.json(
{ error: 'Missing required field: campaignId' },
{ status: 400 }
)
}
// 3. Extract newsletter context (from body or auth)
const newsletterId = body.newsletter_id
// 4. Perform operation
const result = await processData(body, newsletterId)
// 5. Return success response
return NextResponse.json({
success: true,
data: result
})
} catch (error: any) {
// 6. Handle errors
console.error('[API] Error in /api/feature:', error.message)
return NextResponse.json(
{ error: error.message || 'Internal server error' },
{ status: 500 }
)
}
}
// 7. Configure timeout for long-running operations
export const maxDuration = 600 // 10 minutes
ルートの設定
maxDuration の設定
デフォルト:10 秒 最大値:
- Pro プラン:サーバーレスの場合は 300 秒(5 分)
- Pro プラン:Edge Runtime の場合は 900 秒(15 分)
- ワークフローのステップ:800 秒(13 分)
// Short operations (default)
export const maxDuration = 10 // 10 seconds
// Medium operations (API calls, database queries)
export const maxDuration = 60 // 1 minute
// Long operations (RSS processing, content generation)
export const maxDuration = 300 // 5 minutes
// Very long operations (campaign workflow, batch processing)
export const maxDuration = 600 // 10 minutes
// Workflow steps only
export const maxDuration = 800 // 13 minutes (workflow routes only)
ランタイムの設定
// Use Edge Runtime for faster cold starts (limited Node.js APIs)
export const runtime = 'edge'
// Use Node.js runtime for full compatibility (default)
export const runtime = 'nodejs'
// Dynamic route (disable static optimization)
export const dynamic = 'force-dynamic'
HTTP メソッド
GET ルート
export async function GET(request: NextRequest) {
try {
// Extract query parameters
const searchParams = request.nextUrl.searchParams
const campaignId = searchParams.get('campaignId')
const newsletterId = searchParams.get('newsletter_id')
if (!newsletterId) {
return NextResponse.json(
{ error: 'newsletter_id required' },
{ status: 400 }
)
}
// Fetch data
const { data, error } = await supabaseAdmin
.from('newsletter_campaigns')
.select('id, status, date')
.eq('newsletter_id', newsletterId)
.eq('id', campaignId)
.single()
if (error) {
throw new Error(error.message)
}
return NextResponse.json({ data })
} catch (error: any) {
console.error('[API GET] Error:', error.message)
return NextResponse.json(
{ error: error.message },
{ status: 500 }
)
}
}
export const maxDuration = 30
POST ルート(バリデーション付き)
export async function POST(request: NextRequest) {
try {
const body = await request.json()
// Validate input
const validation = validateInput(body)
if (!validation.valid) {
return NextResponse.json(
{ error: validation.error },
{ status: 400 }
)
}
// Process request
const result = await processRequest(body)
return NextResponse.json({
success: true,
data: result
})
} catch (error: any) {
console.error('[API POST] Error:', error.message)
return NextResponse.json(
{ error: error.message },
{ status: 500 }
)
}
}
function validateInput(body: any): { valid: boolean; error?: string } {
if (!body.newsletter_id) {
return { valid: false, error: 'newsletter_id is required' }
}
if (!body.campaignId) {
return { valid: false, error: 'campaignId is required' }
}
return { valid: true }
}
export const maxDuration = 120
PUT ルート(更新)
export async function PUT(request: NextRequest) {
try {
const body = await request.json()
const { id, newsletter_id, ...updates } = body
if (!id || !newsletter_id) {
return NextResponse.json(
{ error: 'id and newsletter_id required' },
{ status: 400 }
)
}
const { data, error } = await supabaseAdmin
.from('articles')
.update(updates)
.eq('id', id)
.eq('newsletter_id', newsletter_id)
.select()
.single()
if (error) {
throw new Error(error.message)
}
return NextResponse.json({ data })
} catch (error: any) {
console.error('[API PUT] Error:', error.message)
return NextResponse.json(
{ error: error.message },
{ status: 500 }
)
}
}
export const maxDuration = 30
DELETE ルート
export async function DELETE(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams
const id = searchParams.get('id')
const newsletterId = searchParams.get('newsletter_id')
if (!id || !newsletterId) {
return NextResponse.json(
{ error: 'id and newsletter_id required' },
{ status: 400 }
)
}
const { error } = await supabaseAdmin
.from('articles')
.delete()
.eq('id', id)
.eq('newsletter_id', newsletterId)
if (error) {
throw new Error(error.message)
}
return NextResponse.json({ success: true })
} catch (error: any) {
console.error('[API DELETE] Error:', error.message)
return NextResponse.json(
{ error: error.message },
{ status: 500 }
)
}
}
export const maxDuration = 30
動的ルート
パラメーター付きルート
// app/api/campaigns/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const campaignId = params.id
const searchParams = request.nextUrl.searchParams
const newsletterId = searchParams.get('newsletter_id')
if (!newsletterId) {
return NextResponse.json(
{ error: 'newsletter_id required' },
{ status: 400 }
)
}
const { data, error } = await supabaseAdmin
.from('newsletter_campaigns')
.select('*')
.eq('id', campaignId)
.eq('newsletter_id', newsletterId)
.single()
if (error) {
throw new Error(error.message)
}
return NextResponse.json({ data })
} catch (error: any) {
console.error('[API] Error:', error.message)
return NextResponse.json(
{ error: error.message },
{ status: 500 }
)
}
}
認証パターン
保護されたルート(サーバーサイド)
import { cookies } from 'next/headers'
import { createServerClient } from '@supabase/ssr'
export async function GET(request: NextRequest) {
try {
// Create Supabase client with cookies
const cookieStore = cookies()
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value
},
},
}
)
// Check authentication
const { data: { user }, error: authError } = await supabase.auth.getUser()
if (authError || !user) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
)
}
// Process authenticated request
const result = await processAuthenticatedRequest(user)
return NextResponse.json({ data: result })
} catch (error: any) {
console.error('[API] Error:', error.message)
return NextResponse.json(
{ error: error.message },
{ status: 500 }
)
}
}
CRON シークレットの検証
export async function GET(request: NextRequest) {
try {
// Validate CRON secret
const authHeader = request.headers.get('authorization')
const cronSecret = process.env.CRON_SECRET
if (!cronSecret || authHeader !== `Bearer ${cronSecret}`) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
)
}
// Process cron job
const result = await processCronJob()
return NextResponse.json({
success: true,
data: result
})
} catch (error: any) {
console.error('[CRON] Error:', error.message)
return NextResponse.json(
{ error: error.message },
{ status: 500 }
)
}
}
export const maxDuration = 300
エラー処理パターン
標準的なエラーハンドラー
function handleApiError(error: any, context: string) {
console.error(`[API Error - ${context}]`, {
message: error.message,
stack: error.stack,
timestamp: new Date().toISOString()
})
// Return user-friendly error
return NextResponse.json(
{
error: error.message || 'An unexpected error occurred',
context: context
},
{ status: error.status || 500 }
)
}
// Usage
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const result = await processData(body)
return NextResponse.json({ data: result })
} catch (error: any) {
return handleApiError(error, 'POST /api/feature')
}
}
バリデーションエラーパターン
class ValidationError extends Error {
status = 400
constructor(message: string) {
super(message)
this.name = 'ValidationError'
}
}
function validateRequest(body: any) {
if (!body.newsletter_id) {
throw new ValidationError('newsletter_id is required')
}
if (!body.campaignId) {
throw new ValidationError('campaignId is required')
}
// More validations...
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
validateRequest(body)
const result = await processData(body)
return NextResponse.json({ data: result })
} catch (error: any) {
const status = error.status || 500
return NextResponse.json(
{ error: error.message },
{ status }
)
}
}
レスポンスパターン
成功レスポンス
return NextResponse.json({
success: true,
data: result,
timestamp: new Date().toISOString()
})
エラーレスポンス
return NextResponse.json(
{
error: 'Descriptive error message',
code: 'ERROR_CODE',
details: additionalInfo
},
{ status: 400 }
)
ページネーション付きレスポンス
return NextResponse.json({
data: items,
pagination: {
page: currentPage,
limit: pageSize,
total: totalItems,
hasMore: hasNextPage
}
})
ヘッダーと CORS
カスタムヘッダーの設定
export async function GET(request: NextRequest) {
const data = await fetchData()
return NextResponse.json({ data }, {
headers: {
'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=30',
'X-Custom-Header': 'value'
}
})
}
CORS の設定
export async function OPTIONS(request: NextRequest) {
return new NextResponse(null, {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
})
}
export async function POST(request: NextRequest) {
const response = NextResponse.json({ data: result })
response.headers.set('Access-Control-Allow-Origin', '*')
return response
}
ベストプラクティス
✅ 推奨事項:
- 入力パラメーターを必ず検証してください
- 処理時間に応じて適切な maxDuration を使用してください
- テナント単位でスコープされたデータは newsletter_id でフィルタリングしてください
- 一貫性のあるレスポンス形式を返してください
- コンテキストを含めてエラーをログに記録してください
- エラー処理には try-catch を使用してください
- 適切な HTTP ステータスコードを設定してください
- 必要に応じて認証を検証してください
❌ 禁止事項:
- 機密性の高いエラー詳細をクライアントに公開しないでください
- 入力バリデーションを省略しないでください
- 長時間の処理にデフォルトの 10s タイムアウトを使用しないでください
- データベースのエラーをそのまま返さないでください
- newsletter_id の確認を忘れないでください
- エラーログの記録を省略しないでください
- 一貫性のないレスポンス形式を使用しないでください
よく使われるパターン
バッチ処理
export async function POST(request: NextRequest) {
try {
const { items, newsletter_id } = await request.json()
const results = await Promise.all(
items.map(item => processItem(item, newsletter_id))
)
return NextResponse.json({
success: true,
processed: results.length,
results
})
} catch (error: any) {
console.error('[API Batch] Error:', error.message)
return NextResponse.json(
{ error: error.message },
{ status: 500 }
)
}
}
export const maxDuration = 300
ストリーミングレスポンス(長時間の処理向け)
export async function GET(request: NextRequest) {
const stream = new ReadableStream({
async start(controller) {
try {
const items = await fetchLargeDataset()
for (const item of items) {
const chunk = JSON.stringify(item) + '\n'
controller.enqueue(new TextEncoder().encode(chunk))
}
controller.close()
} catch (error) {
controller.error(error)
}
}
})
return new NextResponse(stream, {
headers: {
'Content-Type': 'application/x-ndjson',
'Transfer-Encoding': 'chunked'
}
})
}
export const maxDuration = 600
スキルの状態:有効 ✅ 行数:< 500 ✅ フレームワーク:Next.js 15 App Router ✅
コメント
Markdown 対応