React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

typescriptnext.jsroute-handlersNextRequestNextResponseapi

Typing Route Handlers

Recipe

Type Next.js App Router route handlers with NextRequest, NextResponse, and typed route parameters. Build type-safe API endpoints with proper request/response contracts.

Working Example

// 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 });
}

Deep Dive

How It Works

  • Route handlers export named functions matching HTTP methods: 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.
  • In Next.js 15, route params are wrapped in a Promise and must be awaited. This aligns with the async params pattern used in page components.
  • Route handlers are server-only code. They can safely access databases, environment variables, and server-side secrets.

Variations

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" },
  });
}

TypeScript Notes

  • 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).
  • Route params are always strings. Even numeric-looking params like /api/users/123 have id as string. Parse them explicitly: parseInt(id, 10).

Gotchas

  • In Next.js 15, 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.
  • Route handlers do not have access to the React component tree. They cannot use hooks or context.
  • Returning new Response() instead of NextResponse works but loses Next.js-specific features like cookies() and headers() helpers.

Alternatives

ApproachProsCons
Route Handlers (App Router)Web standard APIs, streaming supportManual validation needed
Server ActionsTighter integration with React formsNot for third-party API consumers
API Routes (Pages Router)Familiar req/res patternLegacy, NextApiRequest is Node-specific
tRPCEnd-to-end type safetyAdditional dependency
External API (Express, Fastify)Full control, independent deploymentSeparate service to maintain

FAQs

What HTTP method exports does a route handler file support?
  • GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
  • Export each as a named async function in the route.ts file.
How do I access query parameters in a route handler?
export async function GET(request: NextRequest) {
  const role = request.nextUrl.searchParams.get("role");
  // role is string | null
}
Why is request.json() typed as Promise<any> and what should I do about it?
  • The Web API Request.json() has no way to know your payload shape at compile time.
  • Always validate or narrow the result before using it (type guard, Zod, or explicit cast after checks).
How do I type dynamic route params in Next.js 15?
type RouteContext = {
  params: Promise<{ id: string }>;
};
 
export async function GET(request: NextRequest, context: RouteContext) {
  const { id } = await context.params;
}
  • Params are wrapped in a Promise and must be awaited.
Gotcha: What happens if I forget to await params in Next.js 15?
  • You get a Promise object instead of the actual params.
  • This is a common migration issue from Next.js 14 where params were synchronous.
What is the satisfies keyword used for in route handlers?
  • It validates that a value matches a type without widening it:
return NextResponse.json({ error: "msg" } satisfies ApiErrorResponse);
  • The shape is checked at compile time, but the runtime value is unchanged.
How do I validate request bodies with Zod in a route handler?
  • Parse the body with 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
Are route params always strings even if they look like numbers?
  • Yes. A param like /api/users/123 gives id as "123" (string).
  • Parse explicitly with parseInt(id, 10) or Number(id) if you need a number.
Gotcha: What happens if the request body is not valid JSON?
  • request.json() throws an error at runtime.
  • Always wrap it in a try/catch for robustness in production route handlers.
Can route handlers use React hooks or access the component tree?
  • No. Route handlers are server-only code that runs outside the React component tree.
  • They cannot use hooks, context, or any client-side React APIs.
What is the difference between returning Response and NextResponse?
  • 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.
How do I write a type guard for a request body?
function isValidBody(body: unknown): body is CreateUserBody {
  return (
    typeof body === "object" &&
    body !== null &&
    "name" in body &&
    typeof (body as CreateUserBody).name === "string"
  );
}