mcpField
N
nextjs-api-routes

nextjs-api-routes

github.com/venture-formations開発160 回インストール9 週間前に更新
リポジトリ

nextjs-api-routes について

nextjs-api-routesは、Next.js 15 App RouterでAPIルートを構築するためのパターン集です。リクエスト処理や認証などを実装する開発者向けです。

主な機能

  1. 01GET、POST、PUT、DELETEのパターン
  2. 02入力検証とエラー処理
  3. 03認証とCRONシークレットの確認
  4. 04ランタイムとタイムアウトの設定
  5. 05CORS、ヘッダー、レスポンス形式

活用シーン

  1. 01App Router APIエンドポイントの作成
  2. 02保護されたルートやCRONルートの追加
  3. 03ページ分割・一括レスポンスの処理
  4. 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 ✅

コメント