skillby Venture-Formations
nextjs-api-routes
Next.js 15 API route patterns, NextRequest, NextResponse, error handling, maxDuration configuration, authentication, request validation, server-side operations, route handlers, and API endpoint best practices. Use when creating API routes, handling requests, configuring timeouts, or building server-side endpoints.
Installs: 0
Used in: 1 repos
Updated: 2d ago
$
npx ai-builder add skill Venture-Formations/nextjs-api-routesInstalls to .claude/skills/nextjs-api-routes/
# Next.js API Routes - Pattern Library
## Purpose
Comprehensive guide for building API routes in Next.js 15 for the AIProDaily platform, including request handling, error management, authentication, and performance optimization.
## When to Use
Automatically activates when:
- Creating new API routes in `app/api/**/*.ts`
- Working with NextRequest/NextResponse
- Configuring route timeouts
- Handling authentication
- Processing API requests
- Building server-side endpoints
---
## Quick Start: API Route Template
### Standard POST Route
```typescript
// 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
```
---
## Route Configuration
### maxDuration Settings
**Default**: 10 seconds
**Maximum**:
- Pro plan: 300 seconds (5 minutes) for serverless
- Pro plan: 900 seconds (15 minutes) for Edge Runtime
- Workflow steps: 800 seconds (13 minutes)
```typescript
// 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 Configuration
```typescript
// 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 Methods
### GET Route
```typescript
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 Route (with validation)
```typescript
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 Route (update)
```typescript
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 Route
```typescript
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
```
---
## Dynamic Routes
### Route with Parameters
```typescript
// 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 }
)
}
}
```
---
## Authentication Patterns
### Protected Route (Server-Side)
```typescript
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 Secret Validation
```typescript
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
```
---
## Error Handling Patterns
### Standard Error Handler
```typescript
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')
}
}
```
### Validation Error Pattern
```typescript
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 }
)
}
}
```
---
## Response Patterns
### Success Response
```typescript
return NextResponse.json({
success: true,
data: result,
timestamp: new Date().toISOString()
})
```
### Error Response
```typescript
return NextResponse.json(
{
error: 'Descriptive error message',
code: 'ERROR_CODE',
details: additionalInfo
},
{ status: 400 }
)
```
### Paginated Response
```typescript
return NextResponse.json({
data: items,
pagination: {
page: currentPage,
limit: pageSize,
total: totalItems,
hasMore: hasNextPage
}
})
```
---
## Headers and CORS
### Set Custom Headers
```typescript
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 Configuration
```typescript
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
}
```
---
## Best Practices
### ✅ DO:
- Always validate input parameters
- Use appropriate maxDuration for operation length
- Filter by newsletter_id for tenant-scoped data
- Return consistent response formats
- Log errors with context
- Use try-catch for error handling
- Set appropriate HTTP status codes
- Validate authentication when needed
### ❌ DON'T:
- Expose sensitive error details to clients
- Skip input validation
- Use default 10s timeout for long operations
- Return raw database errors
- Forget to check newsletter_id
- Skip error logging
- Use inconsistent response formats
---
## Common Patterns
### Batch Processing
```typescript
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
```
### Streaming Response (for long operations)
```typescript
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
```
---
**Skill Status**: ACTIVE ✅
**Line Count**: < 500 ✅
**Framework**: Next.js 15 App Router ✅Quick Install
$
npx ai-builder add skill Venture-Formations/nextjs-api-routesDetails
- Type
- skill
- Author
- Venture-Formations
- Slug
- Venture-Formations/nextjs-api-routes
- Created
- 6d ago