Topic 5 of 47
Route Handlers
Overview
Next.js Route Handlers let you create API endpoints alongside your frontend code in the same project — eliminating the need for a separate Express server for simple backends. They run on Edge or Node.js runtimes.
Syntax
typescript
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
// GET /api/users
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const role = searchParams.get('role') ?? 'all';
const users = await db.users.findMany({ where: { role } });
return NextResponse.json(users);
}
// POST /api/users
export async function POST(request: NextRequest) {
const body = await request.json();
const user = await db.users.create({ data: body });
return NextResponse.json(user, { status: 201 });
}
// Dynamic route: app/api/users/[id]/route.ts
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
await db.users.delete({ where: { id: params.id } });
return NextResponse.json({ message: 'Deleted' }, { status: 200 });
}Common Pitfalls
- Route Handlers with GET requests are cached by default — add cache: 'no-store' or use request.url/cookies to opt out.
- Middleware (middleware.ts) runs on the Edge runtime before any route — use it for auth, redirects, and A/B testing.
- Interview tip: Route Handlers are NOT Server Actions. Server Actions are called from forms/components and can mutate data and revalidate cache.
Real-World Example
A authenticated API route with middleware-style validation:
example
typescript
// app/api/orders/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { verifyToken } from '@/lib/auth';
import { z } from 'zod';
const CreateOrderSchema = z.object({
productId: z.string().uuid(),
quantity: z.number().int().positive().max(100),
addressId: z.string().uuid(),
});
export async function POST(request: NextRequest) {
// 1. Authentication
const token = request.headers.get('Authorization')?.replace('Bearer ', '');
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const user = await verifyToken(token);
if (!user) return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
// 2. Validation
const body = await request.json();
const parsed = CreateOrderSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 });
}
// 3. Business logic
const { productId, quantity, addressId } = parsed.data;
const product = await db.products.findById(productId);
if (product.stock < quantity) {
return NextResponse.json({ error: 'Insufficient stock' }, { status: 409 });
}
const order = await db.orders.create({ userId: user.id, productId, quantity, addressId });
return NextResponse.json(order, { status: 201 });
}