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.
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:
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/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/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:
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:
// 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:
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
- Add authentication with NextAuth.js
- Connect to a database (Prisma + PostgreSQL)
- Implement rate limiting with middleware
Never expose API keys or secrets in client-side code. Use environment variables with the correct prefix.
