Route Handler Pattern

Build robust REST APIs using Next.js App Router route handlers with type-safe validation, authentication, and error handling.

Overview#

Route handlers are the foundation of API development in Next.js 13+. They provide a modern, file-based approach to building RESTful endpoints with full support for streaming, caching, and edge runtime.

When to use:

  • Building REST APIs for external consumers
  • Handling webhooks from third-party services
  • Creating public endpoints for your application
  • Implementing file upload/download endpoints

Key features:

  • File-based routing in app/api/ directory
  • Support for all HTTP methods (GET, POST, PUT, PATCH, DELETE)
  • Built-in request/response helpers
  • Edge runtime support
  • Streaming responses

Code Example#

Basic CRUD Route Handler#

1// app/api/posts/route.ts 2import { prisma } from '@/lib/db' 3import { auth } from '@/lib/auth' 4import { NextRequest, NextResponse } from 'next/server' 5import { z } from 'zod' 6 7const CreatePostSchema = z.object({ 8 title: z.string().min(1).max(200), 9 content: z.string().optional(), 10 published: z.boolean().default(false) 11}) 12 13// GET /api/posts 14export async function GET(request: NextRequest) { 15 const searchParams = request.nextUrl.searchParams 16 const page = parseInt(searchParams.get('page') ?? '1') 17 const limit = parseInt(searchParams.get('limit') ?? '10') 18 19 const posts = await prisma.post.findMany({ 20 where: { published: true }, 21 orderBy: { createdAt: 'desc' }, 22 skip: (page - 1) * limit, 23 take: limit, 24 include: { author: { select: { name: true } } } 25 }) 26 27 return NextResponse.json({ posts, page, limit }) 28} 29 30// POST /api/posts 31export async function POST(request: NextRequest) { 32 try { 33 const { userId } = await auth() 34 if (!userId) { 35 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) 36 } 37 38 const body = await request.json() 39 const validated = CreatePostSchema.parse(body) 40 41 const post = await prisma.post.create({ 42 data: { ...validated, authorId: userId } 43 }) 44 45 return NextResponse.json(post, { status: 201 }) 46 } catch (error) { 47 if (error instanceof z.ZodError) { 48 return NextResponse.json({ error: error.errors }, { status: 400 }) 49 } 50 return NextResponse.json({ error: 'Internal error' }, { status: 500 }) 51 } 52}

Dynamic Route Handler#

1// app/api/posts/[id]/route.ts 2import { prisma } from '@/lib/db' 3import { auth } from '@/lib/auth' 4import { NextRequest, NextResponse } from 'next/server' 5 6type Params = { params: Promise<{ id: string }> } 7 8// GET /api/posts/:id 9export async function GET(request: NextRequest, { params }: Params) { 10 const { id } = await params 11 12 const post = await prisma.post.findUnique({ 13 where: { id }, 14 include: { author: { select: { name: true, email: true } } } 15 }) 16 17 if (!post) { 18 return NextResponse.json({ error: 'Not found' }, { status: 404 }) 19 } 20 21 return NextResponse.json(post) 22} 23 24// PATCH /api/posts/:id 25export async function PATCH(request: NextRequest, { params }: Params) { 26 const { userId } = await auth() 27 if (!userId) { 28 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) 29 } 30 31 const { id } = await params 32 const body = await request.json() 33 34 const post = await prisma.post.findUnique({ where: { id } }) 35 if (!post) { 36 return NextResponse.json({ error: 'Not found' }, { status: 404 }) 37 } 38 if (post.authorId !== userId) { 39 return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) 40 } 41 42 const updated = await prisma.post.update({ 43 where: { id }, 44 data: body 45 }) 46 47 return NextResponse.json(updated) 48} 49 50// DELETE /api/posts/:id 51export async function DELETE(request: NextRequest, { params }: Params) { 52 const { userId } = await auth() 53 if (!userId) { 54 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) 55 } 56 57 const { id } = await params 58 59 const post = await prisma.post.findUnique({ where: { id } }) 60 if (!post || post.authorId !== userId) { 61 return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) 62 } 63 64 await prisma.post.delete({ where: { id } }) 65 66 return new NextResponse(null, { status: 204 }) 67}

Error Handling Wrapper#

1// lib/api-utils.ts 2import { NextRequest, NextResponse } from 'next/server' 3import { ZodError } from 'zod' 4 5type Handler = (req: NextRequest, context?: any) => Promise<NextResponse> 6 7export function withErrorHandling(handler: Handler): Handler { 8 return async (req, context) => { 9 try { 10 return await handler(req, context) 11 } catch (error) { 12 console.error('API Error:', error) 13 14 if (error instanceof ZodError) { 15 return NextResponse.json( 16 { error: 'Validation failed', details: error.errors }, 17 { status: 400 } 18 ) 19 } 20 21 if (error instanceof Error) { 22 if (error.message === 'UNAUTHORIZED') { 23 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) 24 } 25 if (error.message === 'NOT_FOUND') { 26 return NextResponse.json({ error: 'Not found' }, { status: 404 }) 27 } 28 } 29 30 return NextResponse.json( 31 { error: 'Internal server error' }, 32 { status: 500 } 33 ) 34 } 35 } 36}

Usage Instructions#

  1. Create route file: Add a route.ts file in the appropriate app/api/ directory
  2. Export HTTP methods: Export async functions named after HTTP methods (GET, POST, PUT, PATCH, DELETE)
  3. Add validation: Use Zod schemas to validate request bodies
  4. Handle authentication: Check auth state before processing requests
  5. Return appropriate responses: Use NextResponse.json() with proper status codes

Best Practices#

  1. Always validate input - Use Zod or similar library to validate request bodies and query parameters
  2. Implement proper error handling - Return consistent error responses with appropriate status codes
  3. Use authentication middleware - Check auth state at the beginning of protected routes
  4. Keep handlers focused - Each handler should do one thing well
  5. Use TypeScript - Define types for request/response bodies
  6. Log errors - Log server errors for debugging while returning generic messages to clients
  7. Consider rate limiting - Protect endpoints from abuse