A Route Handler is a route.ts file inside app/ that exports functions named after HTTP methods, giving you a real JSON API endpoint inside your Next.js app rather than a page:
// app/api/posts/route.ts
export async function GET() {
const posts = await db.post.findMany();
return Response.json(posts);
}
export async function POST(request: Request) {
const body = await request.json();
const post = await db.post.create({ data: body });
return Response.json(post, { status: 201 });
}
// app/api/posts/[id]/route.ts
export async function GET(request: Request, { params }: { params: { id: string } }) {
const post = await db.post.findUnique({ where: { id: params.id } });
if (!post) return Response.json({ error: "Not found" }, { status: 404 });
return Response.json(post);
}
This is a real Request in, real Response out, full control over status codes, headers, and body shape — the same primitives you'd use to build any HTTP endpoint, just colocated inside your Next.js app instead of a separate backend service.
The deciding question is almost always who's calling this:
fetch to maintain.A genuinely common real-world shape is both at once: a Route Handler as the actual public API surface (for webhooks, external consumers, mobile clients), and Server Actions layered on top of that same data for your own app's own UI, so you're not hand-rolling fetch calls to your own API from your own frontend just because the API happens to already exist.