//
Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Type Next.js App Router route handlers with NextRequest, NextResponse, and typed route parameters. Build type-safe API endpoints with proper request/response contracts.
// app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";
type User = {
id: string;
name: string;
email: string;
};
type CreateUserBody = {
name: string;
email: string;
};
type ApiErrorResponse = {
error: string;
};
// GET handler
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const role = searchParams.get("role");
const users: User[] = await fetchUsersFromDb(role);
return NextResponse.json(users);
}
// POST handler with typed body
export async function POST(request: NextRequest) {
const body: unknown = await request.json();
if (!isValidCreateUserBody(body)) {
return NextResponse.json(
{ error: "Invalid request body" } satisfies ApiErrorResponse,
{ status: 400 }
);
}
const newUser: User = {
id: crypto.randomUUID(),
name: body.name,
email: body.email,
};
// ... save to database
return NextResponse.json(newUser, { status: 201 });
}
// Type guard for request body
function isValidCreateUserBody(body: unknown): body is CreateUserBody {
return (
typeof body === "object" &&
body !== null &&
"name" in body &&
"email" in body &&
typeof (body as CreateUserBody).name === "string" &&
typeof (body as CreateUserBody).email === "string"
);
}// app/api/users/[id]/route.ts - Dynamic route params
import { NextRequest, NextResponse } from "next/server";
type RouteContext = {
params: Promise<{ id: string }>;
};
export async function GET(request: NextRequest, context: RouteContext) {
const { id } = await context.params;
const user = await findUserById(id);
if (!user) {
return NextResponse.json({ error: "User not found" }, { status: 404 });
}
return NextResponse.json(user);
}
export async function DELETE(request: NextRequest, context: RouteContext) {
const { id } = await context.params;
await deleteUserById(id);
return new NextResponse(null, { status: 204 });
}GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS.NextRequest extends the Web Request API with convenience methods like nextUrl.searchParams for query parameter access.NextResponse.json(data, init?) creates a JSON response with proper Content-Type headers. The data parameter accepts any serializable value.params are wrapped in a Promise and must be awaited. This aligns with the async params pattern used in page components.Zod validation in route handlers:
import { z } from "zod";
const CreateUserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
});
export async function POST(request: NextRequest) {
const body = await request.json();
const parsed = CreateUserSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: "Validation failed", details: parsed.error.flatten() },
{ status: 400 }
);
}
const { name, email } = parsed.data; // Fully typed
// ... create user
}Setting headers and cookies:
export async function GET(request: NextRequest) {
const token = request.cookies.get("session")?.value;
if (!token) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const response = NextResponse.json({ data: "protected" });
response.headers.set("X-Custom-Header", "value");
return response;
}Streaming response:
export async function GET() {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
for (const chunk of ["Hello", " ", "World"]) {
controller.enqueue(encoder.encode(chunk));
await new Promise((r) => setTimeout(r, 100));
}
controller.close();
},
});
return new NextResponse(stream, {
headers: { "Content-Type": "text/plain" },
});
}request.json() returns Promise<any>. Always validate or cast the result before using it.NextResponse.json() accepts any value. Use satisfies to check the shape: NextResponse.json({ error: "msg" } satisfies ApiErrorResponse)./api/users/123 have id as string. Parse them explicitly: parseInt(id, 10).params is a Promise. Forgetting to await it gives you a Promise object instead of the params. This is a common migration issue from Next.js 14.request.json() throws if the body is not valid JSON. Wrap it in a try/catch for robustness.new Response() instead of NextResponse works but loses Next.js-specific features like cookies() and headers() helpers.| Approach | Pros | Cons |
|---|---|---|
| Route Handlers (App Router) | Web standard APIs, streaming support | Manual validation needed |
| Server Actions | Tighter integration with React forms | Not for third-party API consumers |
| API Routes (Pages Router) | Familiar req/res pattern | Legacy, NextApiRequest is Node-specific |
| tRPC | End-to-end type safety | Additional dependency |
| External API (Express, Fastify) | Full control, independent deployment | Separate service to maintain |
GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONSroute.ts file.export async function GET(request: NextRequest) {
const role = request.nextUrl.searchParams.get("role");
// role is string | null
}Request.json() has no way to know your payload shape at compile time.type RouteContext = {
params: Promise<{ id: string }>;
};
export async function GET(request: NextRequest, context: RouteContext) {
const { id } = await context.params;
}Promise and must be awaited.Promise object instead of the actual params.return NextResponse.json({ error: "msg" } satisfies ApiErrorResponse);safeParse and check parsed.success:const parsed = CreateUserSchema.safeParse(await request.json());
if (!parsed.success) {
return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 });
}
const { name, email } = parsed.data; // Fully typed/api/users/123 gives id as "123" (string).parseInt(id, 10) or Number(id) if you need a number.request.json() throws an error at runtime.new Response() works but lacks Next.js-specific helpers like cookies() and headers().NextResponse extends Response with convenience methods. Prefer it for Next.js features.function isValidBody(body: unknown): body is CreateUserBody {
return (
typeof body === "object" &&
body !== null &&
"name" in body &&
typeof (body as CreateUserBody).name === "string"
);
}Reviewed by Chris St. John·Last updated Jul 19, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥