394 lines
11 KiB
TypeScript
394 lines
11 KiB
TypeScript
/**
|
|
* Next.js 16 - Route Handlers (API Endpoints)
|
|
*
|
|
* Route Handlers replace API Routes from Pages Router.
|
|
* File: app/api/[...]/route.ts
|
|
*/
|
|
|
|
import { NextResponse } from 'next/server'
|
|
import { cookies, headers } from 'next/headers'
|
|
|
|
// ============================================================================
|
|
// Example 1: Basic CRUD API
|
|
// ============================================================================
|
|
|
|
// GET /api/posts
|
|
export async function GET() {
|
|
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
|
|
|
|
return NextResponse.json(posts)
|
|
}
|
|
|
|
// POST /api/posts
|
|
export async function POST(request: Request) {
|
|
const body = await request.json()
|
|
|
|
const post = await fetch('https://api.example.com/posts', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
}).then(r => r.json())
|
|
|
|
return NextResponse.json(post, { status: 201 })
|
|
}
|
|
|
|
// ============================================================================
|
|
// Example 2: Dynamic Routes
|
|
// ============================================================================
|
|
|
|
// File: app/api/posts/[id]/route.ts
|
|
|
|
export async function GET(
|
|
request: Request,
|
|
{ params }: { params: Promise<{ id: string }> }
|
|
) {
|
|
const { id } = await params // ✅ Await params in Next.js 16
|
|
|
|
const post = await fetch(`https://api.example.com/posts/${id}`)
|
|
.then(r => r.json())
|
|
.catch(() => null)
|
|
|
|
if (!post) {
|
|
return NextResponse.json(
|
|
{ error: 'Post not found' },
|
|
{ status: 404 }
|
|
)
|
|
}
|
|
|
|
return NextResponse.json(post)
|
|
}
|
|
|
|
export async function PATCH(
|
|
request: Request,
|
|
{ params }: { params: Promise<{ id: string }> }
|
|
) {
|
|
const { id } = await params
|
|
const body = await request.json()
|
|
|
|
const updated = await fetch(`https://api.example.com/posts/${id}`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
}).then(r => r.json())
|
|
|
|
return NextResponse.json(updated)
|
|
}
|
|
|
|
export async function DELETE(
|
|
request: Request,
|
|
{ params }: { params: Promise<{ id: string }> }
|
|
) {
|
|
const { id } = await params
|
|
|
|
await fetch(`https://api.example.com/posts/${id}`, {
|
|
method: 'DELETE',
|
|
})
|
|
|
|
return NextResponse.json({ message: 'Post deleted' }, { status: 200 })
|
|
}
|
|
|
|
// ============================================================================
|
|
// Example 3: Search with Query Parameters
|
|
// ============================================================================
|
|
|
|
// GET /api/search?q=nextjs&limit=10&page=1
|
|
export async function SEARCH(request: Request) {
|
|
const { searchParams } = new URL(request.url)
|
|
const query = searchParams.get('q') || ''
|
|
const limit = parseInt(searchParams.get('limit') || '10')
|
|
const page = parseInt(searchParams.get('page') || '1')
|
|
const offset = (page - 1) * limit
|
|
|
|
const results = await fetch(
|
|
`https://api.example.com/search?q=${query}&limit=${limit}&offset=${offset}`
|
|
).then(r => r.json())
|
|
|
|
return NextResponse.json({
|
|
results: results.items,
|
|
total: results.total,
|
|
page,
|
|
limit,
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Example 4: Authentication with Cookies
|
|
// ============================================================================
|
|
|
|
// POST /api/auth/login
|
|
export async function LOGIN(request: Request) {
|
|
const { email, password } = await request.json()
|
|
|
|
// Verify credentials
|
|
const user = await fetch('https://api.example.com/auth/login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email, password }),
|
|
}).then(r => r.json())
|
|
|
|
if (!user.token) {
|
|
return NextResponse.json(
|
|
{ error: 'Invalid credentials' },
|
|
{ status: 401 }
|
|
)
|
|
}
|
|
|
|
// Set cookie
|
|
const response = NextResponse.json({ success: true })
|
|
response.cookies.set('token', user.token, {
|
|
httpOnly: true,
|
|
secure: process.env.NODE_ENV === 'production',
|
|
sameSite: 'strict',
|
|
maxAge: 60 * 60 * 24 * 7, // 7 days
|
|
})
|
|
|
|
return response
|
|
}
|
|
|
|
// GET /api/auth/me
|
|
export async function ME() {
|
|
const cookieStore = await cookies() // ✅ Await cookies in Next.js 16
|
|
const token = cookieStore.get('token')?.value
|
|
|
|
if (!token) {
|
|
return NextResponse.json(
|
|
{ error: 'Unauthorized' },
|
|
{ status: 401 }
|
|
)
|
|
}
|
|
|
|
const user = await fetch('https://api.example.com/auth/me', {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
}).then(r => r.json())
|
|
|
|
return NextResponse.json(user)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Example 5: Webhook Handler
|
|
// ============================================================================
|
|
|
|
// File: app/api/webhooks/stripe/route.ts
|
|
|
|
import { headers as getHeaders } from 'next/headers'
|
|
|
|
export async function WEBHOOK(request: Request) {
|
|
const body = await request.text()
|
|
const headersList = await getHeaders() // ✅ Await headers in Next.js 16
|
|
const signature = headersList.get('stripe-signature')
|
|
|
|
if (!signature) {
|
|
return NextResponse.json(
|
|
{ error: 'Missing signature' },
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
|
|
// Verify webhook signature (example with Stripe)
|
|
let event
|
|
try {
|
|
event = JSON.parse(body)
|
|
// In production: stripe.webhooks.constructEvent(body, signature, secret)
|
|
} catch (err) {
|
|
return NextResponse.json(
|
|
{ error: 'Invalid payload' },
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
|
|
// Handle event
|
|
switch (event.type) {
|
|
case 'payment_intent.succeeded':
|
|
await handlePaymentSuccess(event.data.object)
|
|
break
|
|
case 'payment_intent.failed':
|
|
await handlePaymentFailure(event.data.object)
|
|
break
|
|
default:
|
|
console.log(`Unhandled event type: ${event.type}`)
|
|
}
|
|
|
|
return NextResponse.json({ received: true })
|
|
}
|
|
|
|
async function handlePaymentSuccess(paymentIntent: any) {
|
|
console.log('Payment succeeded:', paymentIntent.id)
|
|
// Update database, send confirmation email, etc.
|
|
}
|
|
|
|
async function handlePaymentFailure(paymentIntent: any) {
|
|
console.log('Payment failed:', paymentIntent.id)
|
|
// Notify user, log error, etc.
|
|
}
|
|
|
|
// ============================================================================
|
|
// Example 6: Streaming Response
|
|
// ============================================================================
|
|
|
|
// GET /api/stream
|
|
export async function STREAM() {
|
|
const encoder = new TextEncoder()
|
|
|
|
const stream = new ReadableStream({
|
|
async start(controller) {
|
|
for (let i = 0; i < 10; i++) {
|
|
const data = `data: ${JSON.stringify({ count: i })}\n\n`
|
|
controller.enqueue(encoder.encode(data))
|
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
}
|
|
controller.close()
|
|
},
|
|
})
|
|
|
|
return new Response(stream, {
|
|
headers: {
|
|
'Content-Type': 'text/event-stream',
|
|
'Cache-Control': 'no-cache',
|
|
'Connection': 'keep-alive',
|
|
},
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Example 7: File Upload
|
|
// ============================================================================
|
|
|
|
// POST /api/upload
|
|
import { writeFile } from 'fs/promises'
|
|
import { join } from 'path'
|
|
|
|
export async function UPLOAD(request: Request) {
|
|
const formData = await request.formData()
|
|
const file = formData.get('file') as File
|
|
|
|
if (!file) {
|
|
return NextResponse.json(
|
|
{ error: 'No file provided' },
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
|
|
const bytes = await file.arrayBuffer()
|
|
const buffer = Buffer.from(bytes)
|
|
const filename = `${Date.now()}-${file.name}`
|
|
const path = join(process.cwd(), 'public', 'uploads', filename)
|
|
|
|
await writeFile(path, buffer)
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
url: `/uploads/${filename}`,
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Example 8: CORS Configuration
|
|
// ============================================================================
|
|
|
|
export async function OPTIONS() {
|
|
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 CORS_GET() {
|
|
const response = NextResponse.json({ message: 'Hello' })
|
|
|
|
response.headers.set('Access-Control-Allow-Origin', '*')
|
|
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
|
|
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')
|
|
|
|
return response
|
|
}
|
|
|
|
// ============================================================================
|
|
// Example 9: Error Handling
|
|
// ============================================================================
|
|
|
|
export async function ERROR_HANDLING() {
|
|
try {
|
|
const data = await fetch('https://api.example.com/data')
|
|
.then(r => {
|
|
if (!r.ok) throw new Error('API request failed')
|
|
return r.json()
|
|
})
|
|
|
|
return NextResponse.json(data)
|
|
} catch (error) {
|
|
console.error('Error:', error)
|
|
|
|
return NextResponse.json(
|
|
{ error: 'Internal Server Error' },
|
|
{ status: 500 }
|
|
)
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Example 10: Rate Limiting
|
|
// ============================================================================
|
|
|
|
const rateLimitMap = new Map<string, { count: number; resetAt: number }>()
|
|
|
|
export async function RATE_LIMITED() {
|
|
const headersList = await headers()
|
|
const ip = headersList.get('x-forwarded-for') || 'unknown'
|
|
const now = Date.now()
|
|
|
|
const rateLimit = rateLimitMap.get(ip)
|
|
|
|
if (rateLimit) {
|
|
if (now < rateLimit.resetAt) {
|
|
if (rateLimit.count >= 10) {
|
|
return NextResponse.json(
|
|
{ error: 'Too many requests' },
|
|
{ status: 429 }
|
|
)
|
|
}
|
|
rateLimit.count++
|
|
} else {
|
|
rateLimitMap.set(ip, { count: 1, resetAt: now + 60000 })
|
|
}
|
|
} else {
|
|
rateLimitMap.set(ip, { count: 1, resetAt: now + 60000 })
|
|
}
|
|
|
|
return NextResponse.json({ message: 'Success' })
|
|
}
|
|
|
|
/**
|
|
* Summary:
|
|
*
|
|
* Route Handlers (app/api/*/route.ts):
|
|
* 1. ✅ Support all HTTP methods (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS)
|
|
* 2. ✅ Await params in Next.js 16
|
|
* 3. ✅ Access cookies with await cookies()
|
|
* 4. ✅ Access headers with await headers()
|
|
* 5. ✅ Use NextResponse.json() for JSON responses
|
|
* 6. ✅ Return Response or NextResponse
|
|
*
|
|
* Common patterns:
|
|
* - CRUD operations (GET, POST, PATCH, DELETE)
|
|
* - Query parameters with searchParams
|
|
* - Authentication with cookies
|
|
* - Webhooks with signature verification
|
|
* - Streaming responses (SSE, WebSocket)
|
|
* - File uploads with FormData
|
|
* - CORS configuration
|
|
* - Error handling
|
|
* - Rate limiting
|
|
*
|
|
* Best practices:
|
|
* - Use try/catch for error handling
|
|
* - Return appropriate HTTP status codes
|
|
* - Validate input data
|
|
* - Set secure cookie options in production
|
|
* - Add rate limiting for public endpoints
|
|
* - Use CORS headers when needed
|
|
*/
|