
Next.js route.js
route.js is the file convention behind Route Handlers — the App Router's mechanism for defining custom request handlers for a given route, built directly on the Web Request and Response APIs rather than a Next.js-specific abstraction layered on top of them. If you've written a Fetch-API-based server anywhere else, the mental model here transfers almost directly; the interesting parts of this reference are the Next.js-specific conveniences layered around that Web-standard core.
Minimal Example
export async function GET() {
return Response.json({ message: "Hello World" });
}
Supported HTTP Methods
A route.ts file can export any combination of: GET, HEAD, POST, PUT, DELETE, PATCH, and OPTIONS.
export async function GET(request: Request) {}
export async function HEAD(request: Request) {}
export async function POST(request: Request) {}
export async function PUT(request: Request) {}
export async function DELETE(request: Request) {}
export async function PATCH(request: Request) {}
// If OPTIONS is not defined, Next.js implements it automatically,
// setting the appropriate `Allow` header based on the other methods you've exported.
export async function OPTIONS(request: Request) {}
That automatic OPTIONS behavior is worth knowing about explicitly — you generally don't need to hand-write a preflight handler just to get a correct Allow header; Next.js derives it from whichever methods you've actually defined.
Parameters
request (optional)
The request argument is typed as NextRequest — an extension of the Web Request API — giving you extra conveniences like an easy .cookies accessor and a pre-parsed nextUrl object, on top of everything the standard Request already provides.
import type { NextRequest } from "next/server";
export async function GET(request: NextRequest) {
const url = request.nextUrl;
}
context (optional) — params
The second argument's params field is a promise resolving to this route's dynamic segment values:
export async function GET(
request: Request,
{ params }: { params: Promise<{ team: string }> },
) {
const { team } = await params;
}
| Example | URL | params |
|---|---|---|
app/dashboard/[team]/route.js | /dashboard/1 | Promise<{ team: '1' }> |
app/shop/[tag]/[item]/route.js | /shop/1/2 | Promise<{ tag: '1', item: '2' }> |
app/blog/[...slug]/route.js | /blog/1/2 | Promise<{ slug: ['1', '2'] }> |
Rather than hand-writing that context type each time, the globally-available RouteContext<'/route'> helper infers it directly from your route's file-system position — generated automatically during next dev, next build, or next typegen:
import type { NextRequest } from "next/server";
export async function GET(_req: NextRequest, ctx: RouteContext<"/users/[id]">) {
const { id } = await ctx.params;
return Response.json({ id });
}
Cookies
Read or write cookies via cookies() from next/headers:
import { cookies } from "next/headers";
export async function GET(request: NextRequest) {
const cookieStore = await cookies();
const a = cookieStore.get("a");
cookieStore.set("b", "1");
cookieStore.delete("c");
}
Alternatively, set outgoing cookies directly via the Set-Cookie response header, or read incoming cookies straight off the NextRequest instance without going through next/headers at all:
export async function GET(request: NextRequest) {
const token = request.cookies.get("token");
}
Headers
Read incoming headers with headers() from next/headers — note this returned instance is read-only. To send new headers, you have to construct and return an entirely new Response, not mutate the one you read from:
import { headers } from "next/headers";
export async function GET(request: Request) {
const headersList = await headers();
const referer = headersList.get("referer");
return new Response("Hello, Next.js!", {
status: 200,
headers: { referer },
});
}
Revalidating Cached Data
Route Handlers support the standard route segment config, including revalidate, for time-based cache invalidation of data fetched inside them:
export const revalidate = 60;
export async function GET() {
const data = await fetch("https://api.vercel.app/blog");
const posts = await data.json();
return Response.json(posts);
}
Redirects and Dynamic Segments
redirect() from next/navigation works here just as it does in pages, and Route Handlers use the same Dynamic Segment convention ([slug]) as pages and layouts do:
export async function GET(
request: Request,
{ params }: { params: Promise<{ slug: string }> },
) {
const { slug } = await params; // 'a', 'b', or 'c'
}
You can also combine dynamic Route Handlers with generateStaticParams to prerender responses for known params at build time, while unlisted params still resolve dynamically at request time — and under Cache Components, pairing generateStaticParams with use cache extends that data caching to both prerendered and runtime params alike.
URL Query Parameters
NextRequest's nextUrl gives you a pre-parsed searchParams object without manually constructing a URL yourself:
import { type NextRequest } from "next/server";
export function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const query = searchParams.get("query"); // "hello" for /api/search?query=hello
}
Reading the Request Body
Standard Web API methods handle both JSON and form-encoded bodies:
export async function POST(request: Request) {
const res = await request.json();
return Response.json({ res });
}
export async function POST(request: Request) {
const formData = await request.formData();
const name = formData.get("name");
const email = formData.get("email");
return Response.json({ name, email });
}
Since FormData values always come through as strings, a validation library like zod-form-data is worth reaching for if you need typed coercion (numbers, booleans) rather than parsing everything by hand.
Streaming
Route Handlers can stream responses, which is the standard pattern for AI SDK integrations and any other server-sent, progressively-generated content:
import { openai } from "@ai-sdk/openai";
import { StreamingTextResponse, streamText } from "ai";
export async function POST(req: Request) {
const { messages } = await req.json();
const result = await streamText({ model: openai("gpt-4-turbo"), messages });
return new StreamingTextResponse(result.toAIStream());
}
Or, using the underlying Web APIs directly, without an SDK abstraction in between:
function iteratorToStream(iterator: any) {
return new ReadableStream({
async pull(controller) {
const { value, done } = await iterator.next();
if (done) controller.close();
else controller.enqueue(value);
},
});
}
async function* makeIterator() {
const encoder = new TextEncoder();
yield encoder.encode("<p>One</p>");
yield encoder.encode("<p>Two</p>");
yield encoder.encode("<p>Three</p>");
}
export async function GET() {
return new Response(iteratorToStream(makeIterator()));
}
CORS
Set CORS headers directly with standard Web API response headers for a single Route Handler:
export async function GET(request: Request) {
return new Response("Hello, Next.js!", {
status: 200,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
},
});
}
If you need the same CORS configuration applied across many Route Handlers at once, Proxy or the next.config.js headers option are the better tools — hand-repeating this block in every route file doesn't scale.
Webhooks
A common, genuinely useful pattern — reading raw request text and handling provider-specific verification/parsing yourself:
export async function POST(request: Request) {
try {
const text = await request.text();
// Process the webhook payload
} catch (error) {
return new Response(`Webhook error: ${error.message}`, { status: 400 });
}
return new Response("Success!", { status: 200 });
}
Unlike Pages Router API Routes, there's no bodyParser configuration needed here at all — the Web Request API gives you raw access to the body by default.
Non-UI Responses
Route Handlers aren't limited to JSON — you can return any content type, useful for hand-rolled feeds or formats the built-in metadata conventions don't cover:
export async function GET() {
return new Response(
`<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
<title>Next.js Documentation</title>
<link>https://nextjs.org/docs</link>
<description>The React Framework for the Web</description>
</channel>
</rss>`,
{ headers: { "Content-Type": "text/xml" } },
);
}
Worth knowing before you build one of these by hand: sitemap.xml, robots.txt, app icons, and Open Graph images all have built-in file conventions specifically for generating them — reach for those first rather than hand-rolling a Route Handler for content Next.js already has a dedicated, purpose-built mechanism for.
Segment Config Options
Route Handlers accept the same route segment configuration as pages and layouts:
export const dynamic = "auto";
export const dynamicParams = true;
export const revalidate = false;
export const fetchCache = "auto";
export const runtime = "nodejs";
export const preferredRegion = "auto"; // deprecated
Version History
| Version | Changes |
|---|---|
v15.0.0-RC | context.params became a promise; a codemod is available for migration |
v15.0.0-RC | The default caching behavior for GET handlers changed from static to dynamic |
v13.2.0 | Route Handlers introduced |
Key Takeaways
| Feature | Detail |
|---|---|
| Supported methods | GET, HEAD, POST, PUT, DELETE, PATCH, OPTIONS — OPTIONS auto-generated if omitted |
request | A NextRequest, extending the Web Request API with cookies/URL conveniences |
params | A promise, same convention as pages/layouts — type it with RouteContext<'/route'> |
| Cookies/headers | Read via next/headers, or directly off NextRequest; response headers require returning a new Response |
| Streaming | Fully supported via Web Streams — the standard pattern for AI SDK integrations |
| Built-in alternatives | sitemap.xml, robots.txt, icons, and OG images already have dedicated conventions — don't hand-roll them here |
Default GET caching | Dynamic by default since v15 (a change from the earlier static default) |
route.js is deliberately unopinionated compared to the rest of the App Router's conventions — it hands you the Web platform's own Request/Response primitives and layers just enough Next.js-specific convenience (cookies, nextUrl, segment config, static generation for dynamic routes) on top to make building a real API inside your app frictionless, without hiding the underlying standard from you.


