A production-shaped Next.js route handler
The App Router replaced pages/api with file-based route handlers: a route.ts file exports one async function per HTTP method. A solid handler parses input, validates it, does its work inside a try/catch, and returns typed JSON with the right status. This builder assembles that structure for the methods you choose.
How it works
The tool generates an app/api/<resource>/route.ts. It imports { NextRequest, NextResponse } from 'next/server'. For each selected method it exports an async function: GET returns a list or item with NextResponse.json(data, { status: 200 }); POST awaits request.json(), runs a validation stub, and returns 201 on success or 400 on bad input; PUT and DELETE follow the same parse-validate-respond pattern. Every handler is wrapped in try/catch, logging the error and returning NextResponse.json({ error }, { status: 500 }) so a thrown exception never leaks a stack trace to the client.
App Router vs Pages Router — key differences
In the old pages/api pattern, a single default export handled all methods and you branched on req.method. The App Router replaces that with one named export per method, which is cleaner and allows Next.js to tree-shake unused handlers. The request object is now the standard Web API Request (or NextRequest for extras like cookies), not a Node.js IncomingMessage, so you use await request.json() instead of req.body.
Reading dynamic segments
If your route is at app/api/users/[id]/route.ts, the second argument to each handler is a context object with the segment values:
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const { id } = params;
// fetch user by id
}
Real validation example with Zod
Replace the generated validation stub with a Zod schema for production-quality input checking:
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
const CreateUserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
});
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const result = CreateUserSchema.safeParse(body);
if (!result.success) {
return NextResponse.json(
{ error: result.error.flatten() },
{ status: 400 }
);
}
// result.data is typed and safe to use
return NextResponse.json({ id: 1, ...result.data }, { status: 201 });
} catch {
return NextResponse.json({ error: "Invalid request" }, { status: 500 });
}
}
Practical tips
- Keep handlers thin — delegate business logic to a service module so the route file is only about HTTP parsing and response shaping.
- Use
201 Createdafter a successful POST and return the created resource in the body so callers do not need a second GET. - For the Edge runtime, export
export const runtime = 'edge'— but verify your code avoids Node-only APIs likefsfirst. - Never return a raw error stack trace to the client; catch all exceptions and return a clean
{ error: string }at500.