route.js

Creates HTTP endpoints for a URL by exporting handlers that take a Request and return a Response.

Signature
export async function GET(
  request: NextRequest,
  context: { params: Promise<{ team: string }> }
): Promise<Response>
  • Added in Next.js v13.2.0

Parameters

ParameterTypeDescription
$requestoptionalNextRequestThe 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.js and page.js — the URL can only have one owner.
  • context.params is a Promise since 15 and must be awaited.
  • GET is dynamic by default since 15. Opt back in with export const revalidate = 60.
  • Omitting OPTIONS is usually right — Next.js implements it and sets Allow from the methods you defined.
  • headers() and cookies() are async, and the headers() instance is read-only; set headers on the returned Response.

Example

Example · typescript
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.

Example · typescript
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.

Example · typescript
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.

Example · typescript
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.

Example · typescript
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

  • Be the first to comment on this lesson.