mirror of
https://github.com/greedychipmunk/agent-skills.git
synced 2026-09-14 20:46:42 +08:00
9.3 KiB
9.3 KiB
Next.js API Routes (Route Handlers)
Overview
Route Handlers allow you to create custom request handlers for a given route using the Web Request and Response APIs.
Basic Structure
File Convention
app/
└── api/
├── route.ts # /api
├── posts/
│ └── route.ts # /api/posts
└── posts/
└── [id]/
└── route.ts # /api/posts/[id]
HTTP Methods
// app/api/posts/route.ts
export async function GET(request: Request) {}
export async function POST(request: Request) {}
export async function PUT(request: Request) {}
export async function PATCH(request: Request) {}
export async function DELETE(request: Request) {}
export async function HEAD(request: Request) {}
export async function OPTIONS(request: Request) {}
Request Handling
Reading Request Body
export async function POST(request: Request) {
const body = await request.json()
return Response.json({
received: body,
})
}
Form Data
export async function POST(request: Request) {
const formData = await request.formData()
const name = formData.get("name")
const email = formData.get("email")
return Response.json({ name, email })
}
URL Search Params
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const query = searchParams.get("q")
const page = searchParams.get("page") || "1"
return Response.json({ query, page })
}
Headers
import { headers } from "next/headers"
export async function GET() {
const headersList = headers()
const authorization = headersList.get("authorization")
return Response.json({ authorization })
}
Cookies
import { cookies } from "next/headers"
export async function GET() {
const cookieStore = cookies()
const token = cookieStore.get("token")
return Response.json({ token: token?.value })
}
export async function POST() {
const cookieStore = cookies()
cookieStore.set("session", "abc123", {
httpOnly: true,
secure: true,
sameSite: "strict",
maxAge: 60 * 60 * 24, // 1 day
})
return Response.json({ success: true })
}
Response Handling
JSON Response
export async function GET() {
return Response.json({ message: "Hello" })
}
// With status code
export async function POST() {
return Response.json(
{ message: "Created" },
{ status: 201 }
)
}
Using NextResponse
import { NextResponse } from "next/server"
export async function GET() {
return NextResponse.json(
{ message: "Hello" },
{
status: 200,
headers: {
"Cache-Control": "max-age=3600",
},
}
)
}
Redirects
import { redirect } from "next/navigation"
import { NextResponse } from "next/server"
export async function GET() {
// Option 1: Using redirect function
redirect("/new-location")
// Option 2: Using NextResponse
return NextResponse.redirect(new URL("/new-location", request.url))
}
Streaming Response
export async function GET() {
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 10; i++) {
controller.enqueue(encoder.encode(`data: ${i}\n\n`))
await new Promise((r) => setTimeout(r, 100))
}
controller.close()
},
})
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
})
}
Dynamic Routes
Single Parameter
// app/api/posts/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const post = await getPost(params.id)
if (!post) {
return Response.json(
{ error: "Not found" },
{ status: 404 }
)
}
return Response.json(post)
}
Multiple Parameters
// app/api/users/[userId]/posts/[postId]/route.ts
export async function GET(
request: Request,
{ params }: { params: { userId: string; postId: string } }
) {
const { userId, postId } = params
const post = await getUserPost(userId, postId)
return Response.json(post)
}
Catch-All Routes
// app/api/[...slug]/route.ts
export async function GET(
request: Request,
{ params }: { params: { slug: string[] } }
) {
// /api/a/b/c -> slug = ['a', 'b', 'c']
const path = params.slug.join("/")
return Response.json({ path })
}
CORS
// app/api/route.ts
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
}
export async function OPTIONS() {
return Response.json({}, { headers: corsHeaders })
}
export async function GET() {
return Response.json(
{ message: "Hello" },
{ headers: corsHeaders }
)
}
Caching
Static Route Handler
// Cached by default when using GET with no dynamic features
export async function GET() {
const data = await fetch("https://api.example.com/data")
return Response.json(data)
}
Opt Out of Caching
// Option 1: Use Request object
export async function GET(request: Request) {
// Using request opts out of caching
return Response.json({ time: Date.now() })
}
// Option 2: Route segment config
export const dynamic = "force-dynamic"
export async function GET() {
return Response.json({ time: Date.now() })
}
Revalidation
export const revalidate = 3600 // Revalidate every hour
export async function GET() {
const data = await fetch("https://api.example.com/data")
return Response.json(data)
}
Error Handling
Basic Error Handling
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
try {
const post = await getPost(params.id)
if (!post) {
return Response.json(
{ error: "Post not found" },
{ status: 404 }
)
}
return Response.json(post)
} catch (error) {
console.error("Error fetching post:", error)
return Response.json(
{ error: "Internal server error" },
{ status: 500 }
)
}
}
Validation Error
import { z } from "zod"
const postSchema = z.object({
title: z.string().min(1).max(100),
content: z.string().min(1),
})
export async function POST(request: Request) {
try {
const body = await request.json()
const data = postSchema.parse(body)
const post = await createPost(data)
return Response.json(post, { status: 201 })
} catch (error) {
if (error instanceof z.ZodError) {
return Response.json(
{ error: "Validation failed", details: error.errors },
{ status: 400 }
)
}
return Response.json(
{ error: "Internal server error" },
{ status: 500 }
)
}
}
Authentication
Protected Route
import { getServerSession } from "next-auth"
import { authOptions } from "@/lib/auth"
export async function GET() {
const session = await getServerSession(authOptions)
if (!session) {
return Response.json(
{ error: "Unauthorized" },
{ status: 401 }
)
}
return Response.json({ user: session.user })
}
API Key Authentication
export async function GET(request: Request) {
const apiKey = request.headers.get("x-api-key")
if (apiKey !== process.env.API_KEY) {
return Response.json(
{ error: "Invalid API key" },
{ status: 401 }
)
}
return Response.json({ data: "protected data" })
}
File Uploads
export async function POST(request: Request) {
const formData = await request.formData()
const file = formData.get("file") as File
if (!file) {
return Response.json(
{ error: "No file provided" },
{ status: 400 }
)
}
const bytes = await file.arrayBuffer()
const buffer = Buffer.from(bytes)
// Save to filesystem or cloud storage
const path = `/uploads/${file.name}`
await writeFile(path, buffer)
return Response.json({ path })
}
Webhooks
import crypto from "crypto"
export async function POST(request: Request) {
const body = await request.text()
const signature = request.headers.get("x-webhook-signature")
// Verify signature
const expectedSignature = crypto
.createHmac("sha256", process.env.WEBHOOK_SECRET!)
.update(body)
.digest("hex")
if (signature !== expectedSignature) {
return Response.json(
{ error: "Invalid signature" },
{ status: 401 }
)
}
const event = JSON.parse(body)
// Process webhook event
await processWebhookEvent(event)
return Response.json({ received: true })
}
Best Practices
Structure
- Group related endpoints in folders
- Use descriptive route names
- Keep handlers focused and simple
Security
- Validate all inputs
- Use proper authentication
- Sanitize responses (don't leak sensitive data)
- Implement rate limiting
Performance
- Use appropriate caching
- Optimize database queries
- Return early on errors
Error Handling
- Return appropriate status codes
- Provide helpful error messages
- Log errors for debugging
Last Updated: January 2026