Introduction

In this tutorial we'll build a complete REST API using Route Handlers in the Next.js App Router. Unlike the Pages Router, where you used pages/api/*, you now define endpoints in route.ts files inside the app/ folder.

Make sure you have Node.js 18+ installed before you get started.

Why Route Handlers?

Route Handlers are the natural evolution of API Routes. They let you:

  • Use native HTTP methods (GET, POST, PUT, DELETE)
  • Share logic with Server Components
  • Deploy edge-ready with zero configuration

Creating the Route Handler

Create the file app/api/posts/route.ts:

code
import { NextResponse } from 'next/server'
 
const posts = [
  { id: 1, title: 'Hello World', slug: 'hello-world' },
  { id: 2, title: 'Next.js API', slug: 'nextjs-api' }
]
 
export async function GET() {
  return NextResponse.json({ posts })
}
 
export async function POST(request: Request) {
  const body = await request.json()
  const newPost = { id: posts.length + 1, ...body }
  posts.push(newPost)
  return NextResponse.json(newPost, { status: 201 })
}
Pages Router
// pages/api/posts.ts (Pages Router)\nexport default function handler(req, res) {\n  if (req.method === 'GET') {\n    res.status(200).json({ posts: [] })\n  }\n}
App Router
// app/api/posts/route.ts (App Router)\nexport async function GET() {\n  return Response.json({ posts: [] })\n}

Validation with Zod

For robust APIs, always validate input:

code
import { z } from 'zod'
 
const PostSchema = z.object({
  title: z.string().min(3).max(100),
  slug: z.string().regex(/^[a-z0-9-]+$/)
})
 
export async function POST(request: Request) {
  const body = await request.json()
  const result = PostSchema.safeParse(body)
 
  if (!result.success) {
    return Response.json({ error: result.error.flatten() }, { status: 400 })
  }
 
  return Response.json(result.data, { status: 201 })
}

Use safeParse instead of parse to avoid unhandled exceptions in your APIs.

Dynamic Routes

For endpoints with dynamic parameters, use brackets in the folder name:

code
// app/api/posts/[slug]/route.ts
export async function GET(_request: Request, { params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params
  const post = posts.find((p) => p.slug === slug)
 
  if (!post) {
    return Response.json({ error: 'Not found' }, { status: 404 })
  }
 
  return Response.json(post)
}

Middleware and CORS

If you need CORS to consume the API from another domain:

code
export async function GET() {
  return new Response(JSON.stringify({ posts: [] }), {
    headers: {
      'Content-Type': 'application/json',
      'Access-Control-Allow-Origin': '*'
    }
  })
}

Conclusion

Route Handlers make building APIs in Next.js more intuitive and aligned with Web Standards. With TypeScript, validation, and static generation, you have a complete stack for production.

Next steps

  1. Add authentication with NextAuth.js
  2. Connect to a database (Prisma + PostgreSQL)
  3. Implement rate limiting with middleware

Never expose API keys or secrets in client-side code. Use environment variables with the correct prefix.