Build custom API endpoints using Next.js App Router route handlers with Web Request/Response APIs
Create custom API endpoints in Next.js using the App Router's route handlers with standard Web Request and Response APIs.
This skill guides you through building API endpoints using Next.js route handlers (`route.js`/`route.ts` files). Route handlers allow you to create custom request handlers for specific routes using native Web APIs, supporting all standard HTTP methods (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS).
Create a `route.ts` file in your `app` directory at the desired API path:
```typescript
// app/api/hello/route.ts
export async function GET() {
return Response.json({ message: 'Hello World' })
}
```
The file location determines the API endpoint: `app/api/hello/route.ts` → `/api/hello`
Define exported async functions for each HTTP method you want to support:
```typescript
// app/api/posts/route.ts
import { NextRequest } from 'next/server'
export async function GET(request: NextRequest) {
// Handle GET requests
return Response.json({ posts: [] })
}
export async function POST(request: NextRequest) {
// Handle POST requests
const body = await request.json()
return Response.json({ success: true, data: body })
}
export async function DELETE(request: NextRequest) {
// Handle DELETE requests
return new Response(null, { status: 204 })
}
```
**Supported methods:** GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
**Read cookies:**
```typescript
import { cookies } from 'next/headers'
export async function GET(request: NextRequest) {
const cookieStore = await cookies()
const token = cookieStore.get('token')
// Or from request directly
const tokenAlt = request.cookies.get('token')
}
```
**Read headers:**
```typescript
import { headers } from 'next/headers'
export async function GET(request: NextRequest) {
const headersList = await headers()
const referer = headersList.get('referer')
// Or from request directly
const auth = request.headers.get('authorization')
}
```
**Read URL search params:**
```typescript
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const query = searchParams.get('query')
const page = searchParams.get('page') || '1'
}
```
**Parse request body:**
```typescript
export async function POST(request: NextRequest) {
// JSON body
const json = await request.json()
// FormData
const formData = await request.formData()
const name = formData.get('name')
// Plain text
const text = await request.text()
}
```
Create dynamic API routes using folder names with brackets:
```typescript
// app/api/users/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
return Response.json({ userId: id })
}
```
**Examples:**
```typescript
export async function GET(request: NextRequest) {
const data = { message: 'Success' }
return new Response(JSON.stringify(data), {
status: 200,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, s-maxage=60',
'Set-Cookie': 'token=abc123; Path=/; HttpOnly'
}
})
}
```
**Or use Response.json() helper:**
```typescript
export async function GET() {
return Response.json(
{ success: true },
{
status: 200,
headers: { 'Cache-Control': 'no-store' }
}
)
}
```
```typescript
import { redirect } from 'next/navigation'
export async function GET(request: NextRequest) {
const user = await authenticateUser(request)
if (!user) {
redirect('/login')
}
return Response.json({ user })
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
// Process...
return Response.json({ success: true })
} catch (error) {
return Response.json(
{ error: 'Invalid request' },
{ status: 400 }
)
}
}
```
Add route segment config exports to control caching, revalidation, and runtime:
```typescript
// app/api/posts/route.ts
// Revalidate cached data every 60 seconds
export const revalidate = 60
// Use Edge runtime instead of Node.js
export const runtime = 'edge'
// Force dynamic rendering (disable caching)
export const dynamic = 'force-dynamic'
export async function GET() {
const posts = await fetch('https://api.example.com/posts')
return Response.json(await posts.json())
}
```
```typescript
export async function GET(request: NextRequest) {
const data = { message: 'Hello from API' }
return Response.json(data, {
headers: {
'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 new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}
})
}
```
For dynamic routes that can be statically generated:
```typescript
// app/api/posts/[id]/route.ts
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts')
const data = await posts.json()
return data.map((post) => ({
id: post.id.toString()
}))
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
return Response.json({ id })
}
```
Use `RouteContext` helper for strongly-typed params:
```typescript
// app/api/users/[id]/posts/[postId]/route.ts
import type { NextRequest } from 'next/server'
export async function GET(
_req: NextRequest,
ctx: RouteContext<'/api/users/[id]/posts/[postId]'>
) {
const { id, postId } = await ctx.params
// `id` and `postId` are strongly typed as strings
return Response.json({ userId: id, postId })
}
```
**Authentication middleware:**
```typescript
async function withAuth(request: NextRequest, handler: Function) {
const token = request.headers.get('authorization')
if (!token) {
return Response.json({ error: 'Unauthorized' }, { status: 401 })
}
return handler(request)
}
export async function GET(request: NextRequest) {
return withAuth(request, async (req) => {
return Response.json({ data: 'Protected data' })
})
}
```
**Streaming responses:**
```typescript
export async function GET() {
const encoder = new TextEncoder()
const stream = new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode('Hello '))
controller.enqueue(encoder.encode('streaming!'))
controller.close()
}
})
return new Response(stream)
}
```
**Webhook handling:**
```typescript
export async function POST(request: NextRequest) {
const signature = request.headers.get('x-signature')
const body = await request.text()
// Verify signature
if (!verifySignature(body, signature)) {
return new Response('Invalid signature', { status: 401 })
}
// Process webhook
const event = JSON.parse(body)
await handleWebhook(event)
return new Response('OK')
}
```
Leave a review
No reviews yet. Be the first to review this skill!
# Download SKILL.md from killerskills.ai/api/skills/nextjs-route-handlers/raw