Route handlers (API) cheatsheet.

Basic GET

// app/api/hello/route.ts
import { NextResponse } from "next/server";

export async function GET() {
  return NextResponse.json({ ok: true });
}

All HTTP methods

export async function GET(req: Request) { ... }
export async function POST(req: Request) { ... }
export async function PUT(req: Request) { ... }
export async function DELETE(req: Request) { ... }
export async function PATCH(req: Request) { ... }
export async function OPTIONS(req: Request) { ... }
export async function HEAD(req: Request) { ... }

Exported = handled. Missing = 405.

Dynamic params

// app/api/users/[id]/route.ts
export async function GET(
  _req: Request,
  { params }: { params: Promise<{ id: string }> },
) {
  const { id } = await params;
  const user = await db.user.find(Number(id));
  if (!user) return NextResponse.json({ error: "not found" }, { status: 404 });
  return NextResponse.json(user);
}

Catch-all params

// app/api/files/[...path]/route.ts
export async function GET(
  _req: Request,
  { params }: { params: Promise<{ path: string[] }> },
) {
  const { path } = await params;       // ["a", "b", "c"]
}

Reading JSON body

export async function POST(req: Request) {
  const body = await req.json();
  return NextResponse.json({ received: body });
}

FormData

export async function POST(req: Request) {
  const fd = await req.formData();
  const file = fd.get("file") as File;
  return NextResponse.json({ name: file.name, size: file.size });
}

Search params

export async function GET(req: Request) {
  const url = new URL(req.url);
  const q = url.searchParams.get("q") ?? "";
  const page = Number(url.searchParams.get("page") ?? "1");
  return NextResponse.json({ q, page });
}

Headers

const ua = req.headers.get("user-agent");

const res = NextResponse.json({ ok: true });
res.headers.set("x-custom", "value");
return res;

Cookies

import { cookies } from "next/headers";

export async function POST() {
  const c = await cookies();
  c.set("session", "...", { httpOnly: true });
  return NextResponse.json({ ok: true });
}

// Read:
const session = (await cookies()).get("session")?.value;

Status codes

return NextResponse.json({ error: "bad" }, { status: 400 });
return NextResponse.json({ ok: true }, { status: 201 });
return new NextResponse(null, { status: 204 });

Redirect

return NextResponse.redirect(new URL("/login", req.url));
return NextResponse.redirect("https://other.com");

Streaming

export async function GET() {
  const stream = new ReadableStream({
    start(controller) {
      let i = 0;
      const id = setInterval(() => {
        controller.enqueue(`event: ${i++}\n\n`);
        if (i > 5) { clearInterval(id); controller.close(); }
      }, 1000);
    },
  });
  
  return new NextResponse(stream, {
    headers: { "Content-Type": "text/event-stream" },
  });
}

Server-sent events / streaming responses.

Returning files

export async function GET() {
  const buf = await fs.readFile("./file.pdf");
  return new NextResponse(buf, {
    headers: { "Content-Type": "application/pdf" },
  });
}

Zod validation

import { z } from "zod";

const Body = z.object({ name: z.string(), email: z.string().email() });

export async function POST(req: Request) {
  const parsed = Body.safeParse(await req.json());
  if (!parsed.success) {
    return NextResponse.json({ errors: parsed.error.flatten() }, { status: 400 });
  }
  const user = await db.user.create({ data: parsed.data });
  return NextResponse.json(user, { status: 201 });
}

CORS

const cors = {
  "Access-Control-Allow-Origin": "https://app.example.com",
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE",
  "Access-Control-Allow-Headers": "Content-Type, Authorization",
};

export async function OPTIONS() {
  return new NextResponse(null, { status: 204, headers: cors });
}

export async function GET() {
  return NextResponse.json({}, { headers: cors });
}

Auth in route handler

import { auth } from "@/auth";

export async function GET() {
  const session = await auth();
  if (!session) return NextResponse.json({ error: "unauthorized" }, { status: 401 });
  return NextResponse.json({ user: session.user });
}

Caching responses

export const dynamic = "force-static";
export const revalidate = 60;

export async function GET() {
  const data = await fetchExternal();
  return NextResponse.json(data);
}

Caches the route output. Mostly useful for proxy / data-derived endpoints.

Edge runtime

export const runtime = "edge";

export async function GET(req: Request) {
  return NextResponse.json({ hello: "edge" });
}

Faster cold start, smaller bundle, fewer Node APIs.

Common mistakes

  • Forgetting await params in Next 15+.
  • Returning Response with non-JSON header but JSON body.
  • Missing CORS preflight handler.
  • cache: "force-cache" on per-user data.
  • Not validating body — accepts garbage, crashes.

Read this next

If you want my route handlers + Zod + auth template, it’s at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .