route.js
Creates HTTP endpoints for a URL by exporting handlers that take a Request and return a Response.
export async function GET(
request: NextRequest,
context: { params: Promise<{ team: string }> }
): Promise<Response>- Added in Next.js v13.2.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$requestoptional | NextRequest | The incoming request. NextRequest extends the Web Request API with request.cookies and the parsed request.nextUrl, including nextUrl.searchParams. |
$contextoptional | { params: Promise<{ [key: string]: string | string[] }> } | Second argument holding the route's dynamic parameters. context.params is a Promise and must be awaited. |
Return value
Response | Promise<Response>A Web Response. Response.json() and new Response() both work. A handler must return a Response on every code path — returning nothing makes Next.js error with "No response is returned from route handler. Ensure you return a `Response` or a `NextResponse` in all branches of your handler." rather than sending an empty response.
Errors
Defining both route.js and page.js in the same segment is a conflict, since only one can own the URL. Unsupported HTTP methods return 405.
Changelog
- PHP v15.0.0-RCcontext.params became a Promise; a codemod is available.
- PHP v15.0.0-RCThe default caching for GET handlers changed from static to dynamic — GET handlers are no longer cached unless you opt in.
- PHP v13.2.0Route Handlers introduced.
A route file turns its folder into an HTTP endpoint. Export a function named for each method you accept — GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS — taking a Request and returning a Response. This is the App Router's replacement for pages/api, built on Web APIs rather than the Node request/response pair, so no bodyParser configuration is needed: await request.json() or await request.formData().
Reach for one when the response is not UI — webhooks, an RSS feed, an OAuth callback, an endpoint for a mobile client. For anything your own components consume, prefer fetching directly in a Server Component or a Server Function; a route handler adds a network hop you do not need.
What trips people up
- A folder cannot hold both
route.jsandpage.js— the URL can only have one owner. context.paramsis a Promise since 15 and must be awaited.GETis dynamic by default since 15. Opt back in withexport const revalidate = 60.- Omitting
OPTIONSis usually right — Next.js implements it and setsAllowfrom the methods you defined. headers()andcookies()are async, and theheaders()instance is read-only; set headers on the returnedResponse.
Example
import type { NextRequest } from 'next/server'
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ team: string }> }
) {
const { team } = await params
const query = request.nextUrl.searchParams.get('query')
return Response.json({ team, query })
}
export async function POST(request: NextRequest) {
const body = await request.json()
return Response.json({ received: body }, { status: 201 })
}When to use it
- Receive a Stripe or GitHub webhook and verify its signature
- Serve rss.xml or a JSON feed generated from your database
- Build a search endpoint that reads query parameters off request.nextUrl
- Stream a token-by-token LLM response back to a client
Frequently asked questions
Why is my route.js GET handler not cached anymore?
Because the default changed in v15.0.0-RC: GET handlers are dynamic by default and execute on every request. Opt back into caching with a segment config export such as export const revalidate = 60, or export const dynamic = 'force-static' when the response never changes.
Can page.js and route.js live in the same folder?
No — a single URL cannot be owned by both a page and a route handler, and Next.js reports it as a conflict. Move the endpoint into its own segment, for example app/products/page.tsx for the UI and app/api/products/route.ts for the JSON.
How do I read the query string in a Route Handler?
Use request.nextUrl.searchParams, which gives you a real URLSearchParams instance: request.nextUrl.searchParams.get('query'). The request argument is a NextRequest, an extension of the Web Request API that also adds request.cookies. There is no searchParams prop here — that belongs to page.js.
More examples
Receiving a webhook
The raw body is read with request.text() rather than json(), because signature verification has to hash the exact bytes the sender signed — parsing first and re-serialising changes them. Unlike Pages Router API routes, no bodyParser config is required to get the raw body.
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,
})
}Cached GET with revalidation
Since v15, GET handlers are dynamic by default and run on every request. The revalidate segment config opts back in: the response is cached and regenerated at most once every 60 seconds. Route handlers accept the same segment config exports as pages — dynamic, dynamicParams, revalidate, fetchCache and runtime.
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)
}Returning non-UI content
At app/rss.xml/route.ts the folder name becomes the URL, so this serves /rss.xml with an explicit XML content type. Note that sitemap.xml, robots.txt, app icons and Open Graph images have dedicated metadata file conventions — use those rather than hand-rolling a route handler for them.
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',
},
}
)
}Typed context from a route literal
RouteContext is a global helper generated during next dev, next build or next typegen, so it needs no import. Passing the route literal types ctx.params against the actual folder structure — renaming [id] to [userId] turns into a compile error rather than an undefined at runtime.
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 })
}
Discussion