Concept
The beginner framing: a route.ts file lets you build a custom request handler for a specific path, the App Router's equivalent of a traditional API endpoint, using standard Request and Response objects instead of rendering a React component.
The precise mental model: a route.ts file exports one async function per HTTP method it supports (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS), each receiving a NextRequest (an extension of the standard Request) and returning a Response.
// app/api/users/route.ts
export async function GET(request: NextRequest) {
const url = request.nextUrl; // a parsed URL object, a NextRequest convenience
return Response.json({ message: "Hello World" });
}
export async function POST(request: Request) {
const body = await request.json();
const user = await db.user.create({ data: body });
return Response.json(user, { status: 201 });
}If OPTIONS isn't defined, Next.js automatically implements it, setting the appropriate Allow header based on whichever other methods you've defined.
context.params, the same Promise-based contract as pages
// app/dashboard/[team]/route.ts
export async function GET(
request: Request,
{ params }: { params: Promise<{ team: string }> }
) {
const { team } = await params; // ⚠️ same Promise contract as page/layout params
return Response.json({ team });
}RouteContext<'/route'> is the auto-generated helper type for this, exactly like PageProps/LayoutProps are for pages and layouts.
A route segment is EITHER a page or a Route Handler, never both
A single route segment can have a page.tsx (rendering UI) or a route.ts (a custom handler), not both at once, since they'd both be trying to respond to the exact same path.
Route Handlers vs. Server Actions: picking the right tool
| Route Handler | Server Action | |
|---|---|---|
| Best for | A public API, webhooks, anything called by clients OUTSIDE your own React tree | Mutations tied to your own app's forms/UI |
| Response format | You control it fully, any content type, custom headers/status | Automatically returns fresh UI + result together |
| Called from | Any HTTP client, curl, a webhook provider, a mobile app | Your own <form>/event handlers (though also reachable via direct POST, see Server Actions) |
If the only thing that will ever call this endpoint is your own app's own form, a Server Action is usually the simpler, more direct tool, a Route Handler earns its keep specifically when something outside your React tree needs to talk to it, or when you need response-format control a Server Action's contract doesn't offer.
GET Route Handlers and caching
Under Cache Components, GET Route Handlers follow the same prerendering model as pages, a GET handler with no runtime API access can be prerendered at build time, and "use cache" applies to it exactly as it would to a page (see Caching). generateStaticParams also works with dynamic Route Handlers, prerendering specific API responses at build time for known parameter values.
// app/api/posts/[id]/route.ts
export async function generateStaticParams() {
const posts = await fetch("https://api.vercel.app/blog").then((r) => r.json());
return posts.map((post) => ({ id: `${post.id}` }));
}
export async function GET(request: Request, { params }: RouteContext<"/api/posts/[id]">) {
const { id } = await params;
const post
Try It
Predict what happens before checking the solution.
// app/api/widgets/route.ts
export async function GET() { return Response.json({ widgets: [] }); }
export async function POST() { return Response.json({ created: true }); }
// no OPTIONS definedA client sends an OPTIONS request to /api/widgets. What happens?
Solution
Next.js automatically implements OPTIONS for you, responding with the correct Allow header listing the methods actually defined, in this case, GET, POST (plus HEAD, which is automatically derivable from GET). No manual OPTIONS export is needed unless you want custom behavior beyond the automatic default.
Implement It Yourself
Model the method-to-handler dispatch a Route Handler file conceptually performs:
function createRouteDispatcher(handlers) {
const supportedMethods = Object.keys(handlers);
return async function dispatch(request) {
const method = request.method;
if (method === "OPTIONS" && !handlers.OPTIONS) {
return { status: 204, headers: { Allow: supportedMethods.join(", ") } }; // auto-implemented
}
const handler = handlers[method];
if (!handler) {
return { status: 405, body: "Method Not Allowed" };
}
This mirrors the real mechanism: each exported function name IS the dispatch key, and a missing OPTIONS gets a sensible, automatic default built from whatever methods you did define.
Under the Hood
NextRequest/Response being extensions of the standard Fetch API's Request/Response objects means a Route Handler is, at its core, working with the exact same web-standard interfaces used throughout the rest of the web platform, nothing Next.js-specific about the shape of the request/response themselves. And a GET Route Handler's ability to be prerendered under Cache Components is the identical Caching model applied to a different kind of route segment, the "static shell vs. runtime data" distinction doesn't care whether the segment renders a page or returns JSON.
Common Mistakes
1. Trying to have both page.tsx and route.ts in the same segment
app/products/
page.tsx // ❌ conflicts, pick ONE
route.tsA route segment serves either UI or a custom request handler, never both at the same path, this is a structural conflict, not a configuration option to resolve.
2. Reaching for a Route Handler when a Server Action would be simpler
If the only caller is your own app's own form, a Route Handler adds an extra network round-trip contract (you'd typically fetch it from a Client Component) that a Server Action already handles more directly, in one round-trip, with automatic progressive enhancement built in.
3. Forgetting params is a Promise in Route Handlers too
Exactly the same mistake covered in Dynamic Routes, context.params must be awaited here as well, not accessed synchronously.
Best Practices
- Reach for Route Handlers when something outside your own React tree needs to call this endpoint, a public API, a webhook receiver, a mobile client.
- Reach for Server Actions for mutations tied to your own app's UI, getting the single-round-trip and progressive-enhancement benefits for free.
- Let
GEThandlers with no runtime dependencies be prerendered/cached under Cache Components, exactly like a page, don't assume every API route must be fully dynamic. - Use
RouteContext<'/route'>to correctly typeparamsfor a specific route, rather than hand-writing the shape.
Performance Tips
- A cacheable
GETRoute Handler (no runtime API access) gets the same prerendering benefits as a static page, real, measurable latency savings for API responses that don't need to be computed fresh on every request. generateStaticParamsapplies to dynamic Route Handlers just as it does to pages, prerendering known API responses at build time avoids recomputing them for every request to a well-known set of resources.
