route.ts, and the methods it exports
One file per path, one exported function per method — GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS. Anything you do not export answers 405 by itself.
export async function GET(request) {
const { searchParams } = new URL(request.url);
const q = searchParams.get("q") ?? "";
const doors = await db.doors.search(q);
return Response.json(doors);
}
export async function POST(request) {
const body = await request.json();
const door = await db.doors.insert(body);
return Response.json(door, { status: 201, headers: { Location: `/api/doors/${door.id}` } });
}
Request and Response are the web ones
request.method
request.url // a full absolute URL string
new URL(request.url).searchParams
request.headers.get("authorization")
await request.json()
await request.formData()
await request.text()
Response.json(value, { status: 201 })
new Response("Not found", { status: 404 })
new Response(null, { status: 204 })
new Response(stream, { headers: { "Content-Type": "text/event-stream" } })
These are the objects fetch already gave you, used from the other side. The same handler shape runs on Node, on Deno, on Bun and on edge runtimes — which is why the web standard was chosen.
Dynamic segments and cookies
export async function GET(request, { params }) {
const { id } = await params; // Next 15: params is a Promise
const door = await db.doors.byId(id);
if (!door) return Response.json({ error: "Not found" }, { status: 404 });
return Response.json(door);
}
import { cookies, headers } from "next/headers";
const session = (await cookies()).get("sid")?.value;
Caching applies here too
A handler that returns the same JSON for everyone can be prerendered, exactly like a page. One that reads a session cannot, and Next works that out from what you touched.
export const dynamic = "force-dynamic"; // never cached
export const revalidate = 3600; // regenerate hourly
// reading cookies, headers or searchParams makes it dynamic anyway
When you do not need one
Write a route handler when something outside your React tree needs it: a mobile app, a webhook, a cron job, a public API, an OG image, a file download, a redirect from an old URL.
- Reading data for your own page — do it in the server component. An endpoint your own app calls with
fetchis a round trip you added for nothing. - A form that writes — a server action is less code and works without JavaScript.
- Proxying a third-party API to hide a key — that is what the server component already is.
The ones that are genuinely useful
// a webhook — verify the signature before trusting a byte
export async function POST(request) {
const raw = await request.text();
if (!verify(raw, request.headers.get("stripe-signature"))) {
return new Response("Bad signature", { status: 400 });
}
…
return new Response(null, { status: 204 });
}
// a health check for the platform
export async function GET() { return Response.json({ ok: true }); }
// a file the browser should download
return new Response(csv, {
headers: { "Content-Type": "text/csv", "Content-Disposition": 'attachment; filename="doors.csv"' },
});
// a redirect
return Response.redirect(new URL("/doors", request.url), 308);
Treat it like any public endpoint
- Authenticate and authorise; a handler is reachable by anyone who knows the path.
- Validate the body with a schema before it touches anything.
- Answer with the right status: 400 for malformed, 401/403 for who you are, 404 for missing, 422 for invalid values.
- Rate-limit anything expensive, and never echo an internal error message back.