Building RESTful APIs
Building RESTful APIs
Section titled “Building RESTful APIs”Introduction
Section titled “Introduction”Route Handlers (route.ts) are Next.js’s native way to build API endpoints. They run on the server, support all HTTP methods, and integrate with Next.js’s caching and middleware system.
Basic CRUD API
Section titled “Basic CRUD API”import { NextRequest, NextResponse } from 'next/server'import { db } from '@/lib/db'
// GET /api/posts — List postsexport async function GET(request: NextRequest) { const { searchParams } = new URL(request.url) const page = Number(searchParams.get('page')) || 1 const limit = Number(searchParams.get('limit')) || 10
const posts = await db.post.findMany({ take: limit, skip: (page - 1) * limit, orderBy: { createdAt: 'desc' }, include: { author: { select: { name: true } } }, })
const total = await db.post.count()
return NextResponse.json({ data: posts, pagination: { page, limit, total, totalPages: Math.ceil(total / limit), } })}
// POST /api/posts — Create postexport async function POST(request: NextRequest) { const body = await request.json()
const validated = createPostSchema.safeParse(body) if (!validated.success) { return NextResponse.json( { error: 'Validation failed', details: validated.error.flatten() }, { status: 400 } ) }
const post = await db.post.create({ data: { ...validated.data, authorId: session.user.id } })
return NextResponse.json({ data: post }, { status: 201 })}Dynamic Route Handlers
Section titled “Dynamic Route Handlers”import { NextRequest, NextResponse } from 'next/server'import { db } from '@/lib/db'
// GET /api/posts/123export async function GET( request: NextRequest, { params }: { params: { id: string } }) { const post = await db.post.findUnique({ where: { id: params.id } })
if (!post) { return NextResponse.json( { error: 'Post not found' }, { status: 404 } ) }
return NextResponse.json({ data: post })}
// PUT /api/posts/123export async function PUT( request: NextRequest, { params }: { params: { id: string } }) { const body = await request.json()
const validated = updatePostSchema.safeParse(body) if (!validated.success) { return NextResponse.json({ error: 'Validation failed' }, { status: 400 }) }
const post = await db.post.update({ where: { id: params.id }, data: validated.data, })
return NextResponse.json({ data: post })}
// DELETE /api/posts/123export async function DELETE( request: NextRequest, { params }: { params: { id: string } }) { await db.post.delete({ where: { id: params.id } })
return new NextResponse(null, { status: 204 })}Error Handling Patterns
Section titled “Error Handling Patterns”import { NextResponse } from 'next/server'
async function handleApiError(error: unknown) { console.error('API Error:', error)
if (error instanceof Prisma.PrismaClientKnownRequestError) { if (error.code === 'P2002') { return NextResponse.json( { error: 'A record with this value already exists' }, { status: 409 } ) } if (error.code === 'P2025') { return NextResponse.json( { error: 'Record not found' }, { status: 404 } ) } }
return NextResponse.json( { error: 'Internal server error' }, { status: 500 } )}
// Usageexport async function POST(request: NextRequest) { try { const body = await request.json() const user = await db.user.create({ data: body }) return NextResponse.json({ data: user }, { status: 201 }) } catch (error) { return handleApiError(error) }}API Versioning
Section titled “API Versioning”// app/api/v1/posts/route.ts — v1 (stable)export async function GET() { const posts = await db.post.findMany() return NextResponse.json({ data: posts, version: '1.0' })}
// app/api/v2/posts/route.ts — v2 (new version)export async function GET() { const posts = await db.post.findMany({ include: { author: true, tags: true } }) return NextResponse.json({ data: posts, version: '2.0' })}Request Logging Middleware
Section titled “Request Logging Middleware”import { NextResponse } from 'next/server'import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) { if (request.nextUrl.pathname.startsWith('/api/')) { console.log(`[API] ${request.method} ${request.nextUrl.pathname}`)
const start = Date.now() const response = NextResponse.next()
response.headers.set('X-Response-Time', `${Date.now() - start}ms`) return response }}Common Mistakes
Section titled “Common Mistakes”- Not validating request bodies — Always validate with Zod before processing
- Returning raw database errors — Prisma errors contain sensitive info. Map them to safe messages.
- Missing proper status codes — Use 201 for created, 204 for deleted, 404 for not found
- Not handling JSON parse errors — Wrap
request.json()in try/catch
Best Practices
Section titled “Best Practices”- Use consistent response shapes (
{ data, error, pagination }) - Return appropriate HTTP status codes for each operation
- Validate all inputs with Zod before processing
- Log API errors with enough context for debugging
- Consider API versioning for public endpoints
Summary
Section titled “Summary”RESTful APIs with Route Handlers follow the same patterns as any backend framework: CRUD endpoints, proper status codes, input validation, and error handling. Use NextResponse for JSON responses and handle errors gracefully with structured error responses.