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#
- Create route file: Add a
route.tsfile in the appropriateapp/api/directory - Export HTTP methods: Export async functions named after HTTP methods (
GET,POST,PUT,PATCH,DELETE) - Add validation: Use Zod schemas to validate request bodies
- Handle authentication: Check auth state before processing requests
- Return appropriate responses: Use
NextResponse.json()with proper status codes
Best Practices#
- Always validate input - Use Zod or similar library to validate request bodies and query parameters
- Implement proper error handling - Return consistent error responses with appropriate status codes
- Use authentication middleware - Check auth state at the beginning of protected routes
- Keep handlers focused - Each handler should do one thing well
- Use TypeScript - Define types for request/response bodies
- Log errors - Log server errors for debugging while returning generic messages to clients
- Consider rate limiting - Protect endpoints from abuse
Related Patterns#
- Server Action - Alternative for form submissions and mutations
- Middleware - Request preprocessing and authentication
- Error Handling - Consistent error response formats
- Rate Limiting - Protect APIs from abuse