Type something to search...
Next.js route.js

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;
}
ExampleURLparams
app/dashboard/[team]/route.js/dashboard/1Promise<{ team: '1' }>
app/shop/[tag]/[item]/route.js/shop/1/2Promise<{ tag: '1', item: '2' }>
app/blog/[...slug]/route.js/blog/1/2Promise<{ 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

VersionChanges
v15.0.0-RCcontext.params became a promise; a codemod is available for migration
v15.0.0-RCThe default caching behavior for GET handlers changed from static to dynamic
v13.2.0Route Handlers introduced

Key Takeaways

FeatureDetail
Supported methodsGET, HEAD, POST, PUT, DELETE, PATCH, OPTIONSOPTIONS auto-generated if omitted
requestA NextRequest, extending the Web Request API with cookies/URL conveniences
paramsA promise, same convention as pages/layouts — type it with RouteContext<'/route'>
Cookies/headersRead via next/headers, or directly off NextRequest; response headers require returning a new Response
StreamingFully supported via Web Streams — the standard pattern for AI SDK integrations
Built-in alternativessitemap.xml, robots.txt, icons, and OG images already have dedicated conventions — don't hand-roll them here
Default GET cachingDynamic 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.

Tags :
Share :

Related Posts

Can Next.js Be Used with GraphQL?

Can Next.js Be Used with GraphQL?

Next.js and GraphQL are two powerful technologies that have gained significant traction in the web development community. Next.js, a React-based fram

Dive Deeper
How does Next.js differ from Create React App?

How does Next.js differ from Create React App?

In the world of modern web development, React.js has emerged as a dominant force due to its flexibility, performance, and extensive ecosystem. Two po

Dive Deeper
How does Next.js handle image optimization?

How does Next.js handle image optimization?

In modern web development, image optimization plays a critical role in enhancing user experience and improving site performance. Large, unoptimized i

Dive Deeper