Next.js API Routes vs Route Handlers

Sanjeev SharmaSanjeev Sharma
6 min read

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.

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

FeatureAPI RoutesRoute Handlers
Locationpages/api/*app/api/*
Files[...].ts/.jsroute.ts/.js
RequestNextApiRequestNextRequest
ResponseNextApiResponseNextResponse
MiddlewareLimitedFull middleware support
Async/AwaitSupportedNative async functions

Route Handlers Structure

app/
├── api/
│   ├── posts/
│   │   └── route.tsGET/POST /api/posts
│   ├── posts/
│   │   └── [id]/
│   │       └── route.tsGET/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

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro