Route Handlers
Building actual API endpoints inside the App Router with route.ts and the Web Request/Response APIs.
3 phút đọc
Server Actions cover most data mutations triggered from your own UI, but sometimes you need a genuine HTTP endpoint — something a mobile app can call, a webhook provider can POST to, or a third party can integrate with. That's what Route Handlers are for: API endpoints defined inside the app/ directory itself, right alongside the pages they support.
The route.ts convention
A route.ts (or .js) file inside any folder in app/ turns that folder's URL into an API endpoint instead of a page. You export a function named after the HTTP method it should handle:
// app/api/hello/route.ts
export async function GET(request: Request) {
return Response.json({ message: "Hello from the API" });
}Visiting /api/hello in a browser (a GET request) returns that JSON. Supported method names are GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS — anything else Next.js hasn't been given a handler for automatically returns a 405 Method Not Allowed.
Reading the request body
// app/api/posts/route.ts
export async function POST(request: Request) {
const body = await request.json();
if (!body.title) {
return Response.json({ error: "Title is required" }, { status: 400 });
}
const post = await db.insert(posts).values({ title: body.title });
return Response.json(post, { status: 201 });
}Route Handlers use the standard Web Request and Response APIs — the same objects you'd use in browser fetch code or in a Cloudflare Worker — rather than a Next.js-specific request/response shape. Next.js does extend them with NextRequest/NextResponse for a few convenience helpers (like easy cookie access), but you're never required to use those over the plain Web APIs.
Dynamic segments work the same way as pages
// app/api/posts/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const post = await getPostById(id);
if (!post) {
return Response.json({ error: "Not found" }, { status: 404 });
}
return Response.json(post);
}Just as with page params, the second argument's params is a Promise you need to await.
Route Handlers can't share a URL with a page
You can't have both app/products/page.tsx and app/products/route.ts — each route segment can either render a page or handle raw requests, not both, since a route.ts takes over every HTTP verb for that exact path. This is why API endpoints conventionally live under an app/api/ folder: it keeps them clearly separated from page routes even as the rest of the app grows.
Caching behavior
Route Handlers are not cached by default for the same reason fetch isn't — a GET handler runs fresh on every request unless you explicitly opt in with export const dynamic = "force-static". POST, PUT, PATCH, and DELETE handlers are never cached, regardless of configuration, since caching a mutation would be actively wrong.
Route Handlers vs. Server Actions
If a mutation is only ever triggered from your own app's forms and buttons, a Server Action is usually simpler — no separate endpoint to define, no manual fetch call to write. Reach for a Route Handler specifically when something outside your React component tree needs to call it: a webhook from Stripe, a mobile client, or a public API you're exposing to other developers.