Next.js API Routes vs Route Handlers
Advertisement
Next.js API Routes vs Route Handlers
Next.js provides two ways to create API endpoints. Understanding when to use each is crucial for modern API development.
- Next.js API Routes vs Route Handlers
- Pages Router API Routes (Legacy)
- App Router Route Handlers (Modern)
- Key Differences
- Route Handlers Structure
- Route Handler Methods
- Request Handling
- Dynamic Routes
- Middleware with Route Handlers
- Real-World Example: REST API
- FAQ
Pages Router API Routes (Legacy)
API routes in the Pages Router use the pages/api directory:
// pages/api/posts.ts
import type { NextApiRequest, NextApiResponse } from 'next'
type Data = {
posts?: any[]
error?: string
}
export default function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
if (req.method === 'GET') {
const posts = [{ id: 1, title: 'Hello' }]
res.status(200).json({ posts })
} else if (req.method === 'POST') {
res.status(201).json({ posts: [] })
} else {
res.status(405).json({ error: 'Method not allowed' })
}
}
App Router Route Handlers (Modern)
Route handlers use the App Router and route.ts files:
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const posts = [{ id: 1, title: 'Hello' }]
return NextResponse.json({ posts })
}
export async function POST(request: NextRequest) {
const body = await request.json()
// Create post
return NextResponse.json({ id: 2, ...body }, { status: 201 })
}
Key Differences
| Feature | API Routes | Route Handlers |
|---|---|---|
| Location | pages/api/* | app/api/* |
| Files | [...].ts/.js | route.ts/.js |
| Request | NextApiRequest | NextRequest |
| Response | NextApiResponse | NextResponse |
| Middleware | Limited | Full middleware support |
| Async/Await | Supported | Native async functions |
Route Handlers Structure
app/
├── api/
│ ├── posts/
│ │ └── route.ts → GET/POST /api/posts
│ ├── posts/
│ │ └── [id]/
│ │ └── route.ts → GET/PUT/DELETE /api/posts/[id]
│ └── auth/
│ └── route.ts → /api/auth
Route Handler Methods
// app/api/items/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
return NextResponse.json({ items: [] })
}
export async function POST(request: NextRequest) {
const body = await request.json()
return NextResponse.json({ created: true }, { status: 201 })
}
export async function PUT(request: NextRequest) {
return NextResponse.json({ updated: true })
}
export async function DELETE(request: NextRequest) {
return NextResponse.json({ deleted: true })
}
export async function PATCH(request: NextRequest) {
return NextResponse.json({ patched: true })
}
export async function HEAD(request: NextRequest) {
return new NextResponse(null)
}
export async function OPTIONS(request: NextRequest) {
return new NextResponse(null, {
headers: {
'Allow': 'GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS'
}
})
}
Request Handling
Access request data:
export async function POST(request: NextRequest) {
// Parse JSON body
const body = await request.json()
// Access headers
const contentType = request.headers.get('content-type')
const authorization = request.headers.get('authorization')
// Access query params
const searchParams = request.nextUrl.searchParams
const page = searchParams.get('page')
// Access cookies
const token = request.cookies.get('auth-token')
return NextResponse.json({ received: true })
}
Dynamic Routes
Create parameterized endpoints:
// app/api/posts/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const post = await db.posts.findUnique({ where: { id: params.id } })
if (!post) {
return NextResponse.json(
{ error: 'Post not found' },
{ status: 404 }
)
}
return NextResponse.json(post)
}
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const body = await request.json()
const post = await db.posts.update({
where: { id: params.id },
data: body
})
return NextResponse.json(post)
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
await db.posts.delete({ where: { id: params.id } })
return NextResponse.json({ deleted: true })
}
Middleware with Route Handlers
Route handlers respect middleware:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
export function middleware(request: NextRequest) {
// Protect API routes
if (request.nextUrl.pathname.startsWith('/api/protected')) {
const token = request.cookies.get('auth-token')
if (!token) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
)
}
}
return NextResponse.next()
}
export const config = {
matcher: ['/api/:path*']
}
Real-World Example: REST API
// app/api/blog/posts/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
try {
const { searchParams } = request.nextUrl
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '10')
const posts = await db.posts.findMany({
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: 'desc' }
})
const total = await db.posts.count()
return NextResponse.json({
data: posts,
pagination: { page, limit, total }
})
} catch (error) {
return NextResponse.json(
{ error: 'Failed to fetch posts' },
{ status: 500 }
)
}
}
export async function POST(request: NextRequest) {
try {
// Check auth
const session = await getSession(request)
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
// Validate
if (!body.title || !body.content) {
return NextResponse.json(
{ error: 'Missing required fields' },
{ status: 400 }
)
}
const post = await db.posts.create({
data: {
title: body.title,
content: body.content,
authorId: session.userId
}
})
return NextResponse.json(post, { status: 201 })
} catch (error) {
return NextResponse.json(
{ error: 'Failed to create post' },
{ status: 500 }
)
}
}
// app/api/blog/posts/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const post = await db.posts.findUnique({
where: { id: params.id },
include: { author: true, comments: true }
})
if (!post) {
return NextResponse.json({ error: 'Not found' }, { status: 404 })
}
return NextResponse.json(post)
}
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const session = await getSession(request)
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const post = await db.posts.findUnique({ where: { id: params.id } })
if (!post || post.authorId !== session.userId) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
const body = await request.json()
const updated = await db.posts.update({
where: { id: params.id },
data: body
})
revalidatePath('/blog')
return NextResponse.json(updated)
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const session = await getSession(request)
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const post = await db.posts.findUnique({ where: { id: params.id } })
if (post?.authorId !== session.userId) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
await db.posts.delete({ where: { id: params.id } })
revalidatePath('/blog')
return NextResponse.json({ deleted: true })
}
FAQ
Q: Should I migrate from API routes to route handlers? A: If building new projects, use route handlers. Existing API routes continue to work.
Q: Can I use both in the same app? A: Yes, but avoid having both /pages/api and /app/api for the same route.
Q: What about error handling in route handlers? A: Use try-catch blocks and return appropriate status codes with error responses.
Route handlers provide modern, flexible API development in Next.js 15.
Advertisement