Backend March 1, 2025 · by Florin
Building REST APIs with Next.js Route Handlers
How to build clean API endpoints using Next.js App Router route handlers — CRUD, validation, error handling.
next.jsapitypescriptrest
Route handlers basics
In the Next.js App Router, API routes live in app/api/ as route.ts files. Each file exports HTTP method functions.
// app/api/players/route.ts
import { NextResponse } from 'next/server';
export async function GET() {
const players = await db.player.findMany();
return NextResponse.json(players);
}
export async function POST(request: Request) {
const body = await request.json();
const player = await db.player.create({ data: body });
return NextResponse.json(player, { status: 201 });
}
Dynamic routes
// app/api/players/[id]/route.ts
import { NextResponse } from 'next/server';
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const player = await db.player.findUnique({
where: { id: params.id },
});
if (!player) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
return NextResponse.json(player);
}
export async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
await db.player.delete({ where: { id: params.id } });
return new NextResponse(null, { status: 204 });
}
Input validation
Keep it simple — validate at the boundary:
export async function POST(request: Request) {
const body = await request.json();
if (!body.nickname || typeof body.nickname !== 'string') {
return NextResponse.json(
{ error: 'nickname is required' },
{ status: 400 }
);
}
if (body.nickname.length < 3 || body.nickname.length > 20) {
return NextResponse.json(
{ error: 'nickname must be 3-20 characters' },
{ status: 400 }
);
}
const player = await db.player.create({
data: { nickname: body.nickname },
});
return NextResponse.json(player, { status: 201 });
}
Error handling pattern
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
try {
const player = await db.player.findUniqueOrThrow({
where: { id: params.id },
});
return NextResponse.json(player);
} catch (error) {
if (error.code === 'P2025') {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
return NextResponse.json({ error: 'Internal error' }, { status: 500 });
}
}
Key points
- One
route.tsper endpoint path - Export named functions matching HTTP methods:
GET,POST,PUT,DELETE,PATCH - Use
NextResponse.json()for JSON responses - Set proper status codes
- Validate input at the boundary, trust internal code