关于 nextjs-api-routes
nextjs-api-routes 是面向 Next.js 15 App Router API 路由开发的模式库,供实现请求处理、身份验证及相关路由行为的开发者参考。
主要功能
- 01GET、POST、PUT 和 DELETE 模式
- 02输入验证与错误处理
- 03身份验证和 CRON 密钥检查
- 04运行时与超时配置
- 05CORS、标头和响应格式
适用场景
- 01创建 App 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)
Runtime 配置
// 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