<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged route-handlers</title>
<link>https://coderlegion.com/tag/route-handlers</link>
<description>Powered by Question2Answer</description>
<item>
<title>Route Handlers Explained in Next.js 16 (Beginner's Guide)</title>
<link>https://coderlegion.com/24571/route-handlers-explained-in-next-js-16-beginners-guide</link>
<description>&lt;p&gt;You've built pages, you've built layouts, and now you need something that isn't a page at all — a webhook endpoint for Stripe, a JSON API for a mobile app, or a route that generates a CSV file on demand. None of that fits inside a &lt;code&gt;page.tsx&lt;/code&gt;. That's exactly the gap &lt;strong&gt;Route Handlers&lt;/strong&gt; fill in the Next.js App Router.&lt;/p&gt;
&lt;p&gt;If you've used the old Pages Router, Route Handlers are the direct replacement for &lt;code&gt;pages/api&lt;/code&gt;. If you're coming from Express or another Node framework, think of them as your &lt;code&gt;app.get()&lt;/code&gt; and &lt;code&gt;app.post()&lt;/code&gt; — just organized by folder structure instead of a router file.&lt;/p&gt;
&lt;p&gt;By the end of this guide, you'll know exactly how Route Handlers work in Next.js 16, how they're different from Server Actions, how caching behaves with the new Cache Components model, and how to build a real, typed API endpoint — including handling dynamic routes and webhooks.&lt;/p&gt;
&lt;p&gt;Here's the whole picture at a glance before we dive in step by step:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devstacked.tech/route-handlers-nextjs/image1.svg&quot; alt=&quot;Diagram showing a client request flowing into a Next.js route.ts Route Handler, which optionally calls a database or third-party API, then returns a Response back to the client&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-are-route-handlers-exactly-&quot;&gt;What Are Route Handlers, Exactly?&lt;/h3&gt;
&lt;p&gt;A Route Handler is a function that runs on the server and responds to an HTTP request — no HTML, no React, just data. You define one inside a special file named &lt;code&gt;route.ts&lt;/code&gt;, and it can live anywhere inside your &lt;code&gt;app&lt;/code&gt; directory.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// app/api/hello/route.ts
export async function GET() {
  return Response.json({ message: &quot;Hello from Next.js 16!&quot; })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Visit &lt;code&gt;/api/hello&lt;/code&gt; in your browser, and you'll get back that JSON — no page rendered, just a raw HTTP response.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Route Handlers use the standard Web &lt;code&gt;Request&lt;/code&gt; and &lt;code&gt;Response&lt;/code&gt; APIs — the same ones built into browsers and modern JavaScript runtimes — not the old Express-style &lt;code&gt;req&lt;/code&gt;/&lt;code&gt;res&lt;/code&gt; objects. That means the skills you learn here also apply outside Next.js.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;why-this-is-tricky-for-beginners-&quot;&gt;Why This Is Tricky (For Beginners)&lt;/h3&gt;
&lt;p&gt;New Next.js developers usually hit two points of confusion:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&quot;Isn't this just an API Route?&quot;&lt;/strong&gt; — Sort of, but Route Handlers live in &lt;code&gt;app/&lt;/code&gt; and use a completely different file convention (&lt;code&gt;route.ts&lt;/code&gt; instead of pulling from &lt;code&gt;pages/api&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&quot;Should I use a Route Handler or a Server Action?&quot;&lt;/strong&gt; — Both let you run server-side code, but they solve different problems, and picking the wrong one leads to unnecessary complexity later.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let's clear both of those up before touching any code.&lt;/p&gt;
&lt;h4 id=&quot;route-handlers-vs-pages-router-api-routes&quot;&gt;Route Handlers vs. Pages Router API Routes&lt;/h4&gt;
&lt;p&gt;If you're migrating from the Pages Router, &lt;code&gt;pages/api/hello.ts&lt;/code&gt; becomes &lt;code&gt;app/api/hello/route.ts&lt;/code&gt;. The concept is identical — a folder-based endpoint — but the syntax changes from a single default export to named exports per HTTP method.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Good to know:&lt;/strong&gt; You never need to mix API Routes and Route Handlers in the same project. If your project uses the &lt;code&gt;app&lt;/code&gt; directory, Route Handlers are your only option for API-style endpoints — the Pages Router pattern doesn't apply anymore.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;route-handlers-vs-server-actions&quot;&gt;Route Handlers vs. Server Actions&lt;/h4&gt;
&lt;p&gt;This is the question that trips up almost everyone, so here's the short version:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Route Handlers &lt;/th&gt;&lt;th&gt;Server Actions&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Called from &lt;/td&gt;&lt;td&gt;Anywhere (browser, mobile app, another server, &lt;code&gt;curl&lt;/code&gt;) &lt;/td&gt;&lt;td&gt;Only from inside your own React components&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Best for &lt;/td&gt;&lt;td&gt;Public APIs, webhooks, third-party integrations &lt;/td&gt;&lt;td&gt;Form submissions, internal data mutations&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;HTTP control &lt;/td&gt;&lt;td&gt;Full — you choose methods, headers, status codes &lt;/td&gt;&lt;td&gt;Handled for you automatically&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;URL &lt;/td&gt;&lt;td&gt;Has a real, callable URL &lt;/td&gt;&lt;td&gt;No public URL of its own&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Rule of thumb:&lt;/strong&gt; if something &lt;em&gt;outside&lt;/em&gt; your Next.js app (a webhook from Stripe, a mobile client, a public API consumer) needs to call this code, use a Route Handler. If it's a form or button inside your own app calling a server function, a Server Action is simpler and does less boilerplate for you.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/nextjs-rendering-strategies-explained&quot;&gt;Related: Next.js Rendering Strategies Explained: SSR, SSG, CSR, ISR &amp;amp; Cache Components&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;/h3&gt;
&lt;p&gt;This guide uses:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Next.js 16&lt;/strong&gt; with the App Router&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TypeScript&lt;/strong&gt; in strict mode&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;React Compiler&lt;/strong&gt; enabled (not directly relevant to Route Handlers, but assumed as the project default)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You don't need any extra packages — everything here ships with Next.js out of the box.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-1-the-route-ts-file-convention&quot;&gt;Step 1: The &lt;code&gt;route.ts&lt;/code&gt; File Convention&lt;/h3&gt;
&lt;p&gt;A Route Handler is created by adding a &lt;code&gt;route.ts&lt;/code&gt; (or &lt;code&gt;route.js&lt;/code&gt;) file inside any folder in &lt;code&gt;app/&lt;/code&gt;. The folder path becomes the URL.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;app/
└── api/
    └── hello/
        └── route.ts       → /api/hello
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// app/api/hello/route.ts
export async function GET(request: Request) {
  return Response.json({ message: &quot;Hello, world!&quot; })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; exporting a function named &lt;code&gt;GET&lt;/code&gt; tells Next.js &quot;handle GET requests to this route with this function.&quot; The function receives the standard Web &lt;code&gt;Request&lt;/code&gt; object and must return a &lt;code&gt;Response&lt;/code&gt; (or &lt;code&gt;NextResponse&lt;/code&gt;, which extends it).&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; A &lt;code&gt;route.ts&lt;/code&gt; file &lt;strong&gt;cannot&lt;/strong&gt; live in the same folder as a &lt;code&gt;page.tsx&lt;/code&gt; at the same segment. &lt;code&gt;app/page.tsx&lt;/code&gt; and &lt;code&gt;app/route.ts&lt;/code&gt; together will throw a build error — Next.js doesn't know whether that URL should render a page or run an API handler. Nest your Route Handler under a different segment, like &lt;code&gt;app/api/...&lt;/code&gt;, instead.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;step-2-supported-http-methods&quot;&gt;Step 2: Supported HTTP Methods&lt;/h3&gt;
&lt;p&gt;Route Handlers support the standard HTTP verbs as named exports in the same file: &lt;code&gt;GET&lt;/code&gt;, &lt;code&gt;POST&lt;/code&gt;, &lt;code&gt;PUT&lt;/code&gt;, &lt;code&gt;PATCH&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;, &lt;code&gt;HEAD&lt;/code&gt;, and &lt;code&gt;OPTIONS&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// app/api/todos/route.ts
export async function GET() {
  const todos = await getTodosFromDb()
  return Response.json(todos)
}

export async function POST(request: Request) {
  const body = await request.json()
  const newTodo = await createTodo(body)
  return Response.json(newTodo, { status: 201 })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; one file now handles two different behaviors depending on the incoming HTTP method — a &lt;code&gt;GET&lt;/code&gt; request lists todos, a &lt;code&gt;POST&lt;/code&gt; request creates one. If a client sends a method you haven't exported (like &lt;code&gt;DELETE&lt;/code&gt; on a file with only &lt;code&gt;GET&lt;/code&gt;/&lt;code&gt;POST&lt;/code&gt;), Next.js automatically responds with a &lt;code&gt;405 Method Not Allowed&lt;/code&gt; — you don't need to handle that yourself.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Next.js also auto-implements a sensible &lt;code&gt;OPTIONS&lt;/code&gt; response for you if you don't define one, which is useful for CORS preflight requests (more on that below).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;response-vs-nextresponse-request-vs-nextrequest&quot;&gt;Response vs. NextResponse, Request vs. NextRequest&lt;/h3&gt;
&lt;p&gt;Before going further, it's worth clearing up which object to reach for — they show up interchangeably in examples online, which confuses a lot of beginners.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Request&lt;/code&gt; / &lt;code&gt;Response&lt;/code&gt;&lt;/strong&gt; — the plain Web APIs, built into the JavaScript runtime itself, not Next.js-specific. Use these when you don't need anything extra — a simple JSON response, or reading a body with &lt;code&gt;request.json()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;NextRequest&lt;/code&gt;&lt;/strong&gt; (from &lt;code&gt;next/server&lt;/code&gt;) — extends &lt;code&gt;Request&lt;/code&gt; with Next.js-only conveniences, most notably &lt;code&gt;request.nextUrl&lt;/code&gt; (an already-parsed URL object) and &lt;code&gt;request.cookies&lt;/code&gt;. Reach for this the moment you need query params or cookies without manually parsing the URL string.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;NextResponse&lt;/code&gt;&lt;/strong&gt; (from &lt;code&gt;next/server&lt;/code&gt;) — extends &lt;code&gt;Response&lt;/code&gt; with helpers like &lt;code&gt;NextResponse.redirect()&lt;/code&gt;, &lt;code&gt;NextResponse.rewrite()&lt;/code&gt;, and easier cookie-setting via &lt;code&gt;response.cookies.set()&lt;/code&gt;. Use this when you need to redirect, rewrite, or set cookies on the way out.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; A good default: start with plain &lt;code&gt;Request&lt;/code&gt;/&lt;code&gt;Response.json()&lt;/code&gt; for simple handlers, and upgrade to &lt;code&gt;NextRequest&lt;/code&gt;/&lt;code&gt;NextResponse&lt;/code&gt; only when you actually need one of their extra features. Both pairs are fully interoperable — a function typed to accept &lt;code&gt;NextRequest&lt;/code&gt; still works fine if you never end up using the extra methods.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;step-3-reading-data-from-the-request&quot;&gt;Step 3: Reading Data From the Request&lt;/h3&gt;
&lt;p&gt;Most real endpoints need to read something — a JSON body, a query parameter, or a header.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// app/api/search/route.ts
import { NextRequest } from &quot;next/server&quot;

export async function GET(request: NextRequest) {
  // Reading a query param: /api/search?q=nextjs
  const query = request.nextUrl.searchParams.get(&quot;q&quot;)

  if (!query) {
    return Response.json({ error: &quot;Missing 'q' parameter&quot; }, { status: 400 })
  }

  const results = await searchDatabase(query)
  return Response.json(results)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; &lt;code&gt;NextRequest&lt;/code&gt; (imported from &lt;code&gt;next/server&lt;/code&gt;) extends the standard &lt;code&gt;Request&lt;/code&gt; with convenience helpers — &lt;code&gt;request.nextUrl&lt;/code&gt; gives you an already-parsed URL object, so &lt;code&gt;searchParams.get(&quot;q&quot;)&lt;/code&gt; is easier than manually parsing &lt;code&gt;request.url&lt;/code&gt; with the native &lt;code&gt;URL&lt;/code&gt; class.&lt;/p&gt;
&lt;p&gt;For a JSON body on a &lt;code&gt;POST&lt;/code&gt; or &lt;code&gt;PUT&lt;/code&gt; request, use &lt;code&gt;request.json()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// app/api/contact/route.ts
export async function POST(request: Request) {
  const { email, message } = await request.json()

  if (!email || !message) {
    return Response.json(
      { error: &quot;Email and message are required&quot; },
      { status: 400 }
    )
  }

  await sendContactEmail({ email, message })
  return Response.json({ success: true })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Never trust that &lt;code&gt;email&lt;/code&gt; and &lt;code&gt;message&lt;/code&gt; exist just because your frontend form requires them. Anyone can send a raw &lt;code&gt;POST&lt;/code&gt; request straight to &lt;code&gt;/api/contact&lt;/code&gt; with &lt;code&gt;curl&lt;/code&gt;, skipping your form entirely — always validate on the server, ideally with a schema library like Zod.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;step-4-dynamic-route-segments&quot;&gt;Step 4: Dynamic Route Segments&lt;/h3&gt;
&lt;p&gt;Just like pages, Route Handlers support dynamic segments using square brackets.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;app/
└── users/
    └── [id]/
        └── route.ts        → /users/123, /users/456, etc.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In Next.js 16, the recommended way to type the dynamic segment is the built-in &lt;code&gt;RouteContext&lt;/code&gt; helper — it's generated automatically for you based on your actual route structure.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// app/users/[id]/route.ts
import type { NextRequest } from &quot;next/server&quot;

export async function GET(
  request: NextRequest,
  ctx: RouteContext&amp;lt;&quot;/users/[id]&quot;&amp;gt;
) {
  const { id } = await ctx.params
  const user = await getUserById(id)

  if (!user) {
    return Response.json({ error: &quot;User not found&quot; }, { status: 404 })
  }

  return Response.json(user)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; &lt;code&gt;ctx.params&lt;/code&gt; is a &lt;code&gt;Promise&lt;/code&gt; — this matches the same async &lt;code&gt;params&lt;/code&gt; convention used in &lt;code&gt;page.tsx&lt;/code&gt; files throughout Next.js 16, so you must &lt;code&gt;await&lt;/code&gt; it before reading &lt;code&gt;id&lt;/code&gt;. &lt;code&gt;RouteContext&amp;lt;&quot;/users/[id]&quot;&amp;gt;&lt;/code&gt; gives you full type-safety without manually writing an interface for every dynamic route.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Good to know:&lt;/strong&gt; These types are generated when you run &lt;code&gt;next dev&lt;/code&gt;, &lt;code&gt;next build&lt;/code&gt;, or &lt;code&gt;next typegen&lt;/code&gt; — if your editor shows a type error right after creating a new dynamic route, running one of those commands usually fixes it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;step-5-caching-behavior&quot;&gt;Step 5: Caching Behavior&lt;/h3&gt;
&lt;p&gt;This trips up a lot of people coming from the Pages Router, where API Routes were never cached. In the App Router, it's a little more nuanced.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The short version:&lt;/strong&gt; Route Handlers are &lt;strong&gt;not cached by default&lt;/strong&gt;. Only &lt;code&gt;GET&lt;/code&gt; handlers can opt into caching — every other method (&lt;code&gt;POST&lt;/code&gt;, &lt;code&gt;PUT&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;, etc.) is always dynamic, even if you configure caching in the same file.&lt;/p&gt;
&lt;p&gt;To force a &lt;code&gt;GET&lt;/code&gt; handler to be cached as static:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// app/items/route.ts
export const dynamic = &quot;force-static&quot;

export async function GET() {
  const res = await fetch(&quot;https://data.example.com/items&quot;)
  const data = await res.json()
  return Response.json({ data })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;if-you-re-using-cache-components-use-cache-&quot;&gt;If You're Using Cache Components (&lt;code&gt;&quot;use cache&quot;&lt;/code&gt;)&lt;/h4&gt;
&lt;p&gt;If your project has &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/nextjs-rendering-strategies-explained&quot;&gt;Cache Components&lt;/a&gt; enabled, &lt;code&gt;GET&lt;/code&gt; Route Handlers behave exactly like a normal page: they run dynamically by default, but can be pre-rendered at build time if they don't touch anything request-specific.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// app/api/project-info/route.ts
// No dynamic data — this gets pre-rendered at build time
export async function GET() {
  return Response.json({ projectName: &quot;DevStacked&quot; })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// app/api/user-agent/route.ts
import { headers } from &quot;next/headers&quot;

// Reads a request-specific header — this forces the route to run per-request
export async function GET() {
  const headersList = await headers()
  const userAgent = headersList.get(&quot;user-agent&quot;)
  return Response.json({ userAgent })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you need to mix cached data with per-request logic, pull the cached part into its own function using &lt;code&gt;&quot;use cache&quot;&lt;/code&gt; — you can't put &lt;code&gt;&quot;use cache&quot;&lt;/code&gt; directly inside the Route Handler body itself.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// app/api/products/route.ts
import { cacheLife } from &quot;next/cache&quot;

export async function GET() {
  const products = await getProducts()
  return Response.json(products)
}

async function getProducts() {
  &quot;use cache&quot;
  cacheLife(&quot;hours&quot;)

  return await db.query(&quot;SELECT * FROM products&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Prerendering for a &lt;code&gt;GET&lt;/code&gt; handler stops the moment it touches anything non-deterministic — a database call without &lt;code&gt;&quot;use cache&quot;&lt;/code&gt;, &lt;code&gt;Math.random()&lt;/code&gt;, or runtime APIs like &lt;code&gt;cookies()&lt;/code&gt; and &lt;code&gt;headers()&lt;/code&gt;. If a route you expected to be static is showing up dynamic in your build output, one of these is almost always why.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;a-quick-word-on-the-edge-runtime&quot;&gt;A Quick Word on the Edge Runtime&lt;/h4&gt;
&lt;p&gt;By default, Route Handlers run on the &lt;strong&gt;Node.js runtime&lt;/strong&gt; — the full Node environment, with access to everything you'd expect (the &lt;code&gt;fs&lt;/code&gt; module, native npm packages, longer execution limits). You can opt a specific route into the lighter-weight &lt;strong&gt;Edge Runtime&lt;/strong&gt; instead:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// app/api/geo/route.ts
export const runtime = &quot;edge&quot;

export async function GET() {
  return Response.json({ message: &quot;Running on the Edge&quot; })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Edge Runtime starts faster and runs closer to the user geographically, but it's a stripped-down environment — no &lt;code&gt;fs&lt;/code&gt;, no full Node API surface, and not every npm package supports it. Reach for it for small, latency-sensitive handlers (geolocation checks, lightweight redirects); stick with the Node.js default for anything doing real database work, file access, or using Node-only packages like the Stripe or AWS SDKs.&lt;/p&gt;
&lt;h3 id=&quot;step-6-handling-webhooks-raw-body-access-&quot;&gt;Step 6: Handling Webhooks (Raw Body Access)&lt;/h3&gt;
&lt;p&gt;Webhooks — like Stripe payment confirmations — are one of the most common real-world uses for Route Handlers. The key detail: webhook signature verification needs the &lt;strong&gt;raw, unparsed&lt;/strong&gt; request body, not JSON.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// app/api/webhook/route.ts
import { NextResponse } from &quot;next/server&quot;
import { headers } from &quot;next/headers&quot;
import Stripe from &quot;stripe&quot;
import { stripe } from &quot;@/lib/stripe&quot;

const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET!

export async function POST(req: Request) {
  const payload = await req.text() // raw text, NOT req.json()
  const signature = (await headers()).get(&quot;stripe-signature&quot;)

  if (!signature) {
    return NextResponse.json({ error: &quot;Missing signature&quot; }, { status: 400 })
  }

  let event: Stripe.Event

  try {
    event = stripe.webhooks.constructEvent(payload, signature, endpointSecret)
  } catch (err) {
    const message = err instanceof Error ? err.message : &quot;Unknown error&quot;
    return NextResponse.json({ error: `Webhook Error: ${message}` }, { status: 400 })
  }

  // event is now verified — safe to trust
  return NextResponse.json({ received: true })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; &lt;code&gt;req.text()&lt;/code&gt; reads the body exactly as Stripe sent it. If you call &lt;code&gt;req.json()&lt;/code&gt; first, the body gets re-serialized slightly differently, and the cryptographic signature check fails — a classic gotcha that has nothing to do with your actual secret key.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/stripe-webhooks-idempotency&quot;&gt;Related: Stripe Payment Element in Production: Webhooks, Idempotency &amp;amp; Testing&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;step-7-adding-cors-support&quot;&gt;Step 7: Adding CORS Support&lt;/h3&gt;
&lt;p&gt;If your Route Handler needs to be called from a different domain (a separate frontend, a browser extension, another app), you need to explicitly allow it with CORS headers.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// app/api/public-data/route.ts
export async function GET() {
  return Response.json(
    { data: &quot;available to any origin&quot; },
    {
      headers: {
        &quot;Access-Control-Allow-Origin&quot;: &quot;*&quot;,
      },
    }
  )
}

export async function OPTIONS() {
  return new Response(null, {
    status: 204,
    headers: {
      &quot;Access-Control-Allow-Origin&quot;: &quot;*&quot;,
      &quot;Access-Control-Allow-Methods&quot;: &quot;GET, POST, OPTIONS&quot;,
      &quot;Access-Control-Allow-Headers&quot;: &quot;Content-Type, Authorization&quot;,
    },
  })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Setting CORS headers on &lt;code&gt;GET&lt;/code&gt;/&lt;code&gt;POST&lt;/code&gt; but forgetting &lt;code&gt;OPTIONS&lt;/code&gt;. Browsers send a &quot;preflight&quot; &lt;code&gt;OPTIONS&lt;/code&gt; request before certain cross-origin requests, and if that preflight doesn't return the right headers, the browser blocks the real request before it's ever sent — even if your &lt;code&gt;GET&lt;/code&gt; handler would have worked fine.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If you need CORS applied consistently across many routes at once, handling it in &lt;code&gt;proxy.ts&lt;/code&gt; (formerly &lt;code&gt;middleware.ts&lt;/code&gt;) instead of repeating this in every file is usually cleaner.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;route-resolution-rules-to-know&quot;&gt;Route Resolution Rules to Know&lt;/h3&gt;
&lt;p&gt;A few quick rules worth keeping in mind as your API grows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;route.ts&lt;/code&gt; file &lt;strong&gt;cannot&lt;/strong&gt; exist at the same segment as a &lt;code&gt;page.tsx&lt;/code&gt; — pick one per route.&lt;/li&gt;
&lt;li&gt;Route Handlers &lt;strong&gt;do not&lt;/strong&gt; participate in layouts or client-side navigation the way pages do. They're the lowest-level routing primitive — pure request in, response out.&lt;/li&gt;
&lt;li&gt;Each &lt;code&gt;route.ts&lt;/code&gt; takes over &lt;strong&gt;every&lt;/strong&gt; HTTP verb for that exact segment. A nested dynamic segment like &lt;code&gt;app/[user]/page.tsx&lt;/code&gt; next to &lt;code&gt;app/api/route.ts&lt;/code&gt; is perfectly valid, since they're different paths.&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Page &lt;/th&gt;&lt;th&gt;Route &lt;/th&gt;&lt;th&gt;Result&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;app/page.tsx&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;app/route.ts&lt;/code&gt; &lt;/td&gt;&lt;td&gt;✗ Conflict&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;app/page.tsx&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;app/api/route.ts&lt;/code&gt; &lt;/td&gt;&lt;td&gt;✓ Valid&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;app/[user]/page.tsx&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;app/api/route.ts&lt;/code&gt; &lt;/td&gt;&lt;td&gt;✓ Valid&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;frequently-asked-questions&quot;&gt;Frequently Asked Questions&lt;/h3&gt;
&lt;h4 id=&quot;can-i-use-route-handlers-with-the-pages-router-&quot;&gt;Can I use Route Handlers with the Pages Router?&lt;/h4&gt;
&lt;p&gt;No — Route Handlers only work inside the &lt;code&gt;app&lt;/code&gt; directory. If your project still uses &lt;code&gt;pages/&lt;/code&gt;, you'd build API endpoints with the older &lt;code&gt;pages/api&lt;/code&gt; convention instead, which behaves differently (Express-style &lt;code&gt;req&lt;/code&gt;/&lt;code&gt;res&lt;/code&gt; rather than Web &lt;code&gt;Request&lt;/code&gt;/&lt;code&gt;Response&lt;/code&gt;).&lt;/p&gt;
&lt;h4 id=&quot;do-i-need-route-handlers-if-i-m-already-using-server-actions-&quot;&gt;Do I need Route Handlers if I'm already using Server Actions?&lt;/h4&gt;
&lt;p&gt;Not necessarily — plenty of apps only use Server Actions for internal mutations. Reach for a Route Handler specifically when something outside your React component tree needs a callable URL: a webhook, a public API, or a request from a non-Next.js client like a mobile app.&lt;/p&gt;
&lt;h4 id=&quot;why-isn-t-my-get-route-handler-getting-cached-&quot;&gt;Why isn't my GET Route Handler getting cached?&lt;/h4&gt;
&lt;p&gt;Route Handlers are dynamic by default. You need to explicitly opt in with &lt;code&gt;export const dynamic = &quot;force-static&quot;&lt;/code&gt;, or — if you're on Cache Components — make sure the handler doesn't touch runtime APIs like &lt;code&gt;cookies()&lt;/code&gt;, &lt;code&gt;headers()&lt;/code&gt;, or an uncached database call.&lt;/p&gt;
&lt;h4 id=&quot;can-i-return-something-other-than-json-&quot;&gt;Can I return something other than JSON?&lt;/h4&gt;
&lt;p&gt;Yes. &lt;code&gt;Response&lt;/code&gt; and &lt;code&gt;NextResponse&lt;/code&gt; can return any body type — plain text, HTML, a file buffer, even a streamed response. Just set the appropriate &lt;code&gt;Content-Type&lt;/code&gt; header so the client knows how to interpret it.&lt;/p&gt;
&lt;h4 id=&quot;how-do-i-protect-a-route-handler-so-only-logged-in-users-can-call-it-&quot;&gt;How do I protect a Route Handler so only logged-in users can call it?&lt;/h4&gt;
&lt;p&gt;Check the session inside the handler itself, the same way you would inside a Server Action — never rely on &lt;code&gt;proxy.ts&lt;/code&gt;/middleware alone as your only line of defense. Most auth libraries (Clerk, Auth.js) expose a server-side helper you can call at the top of your &lt;code&gt;GET&lt;/code&gt;/&lt;code&gt;POST&lt;/code&gt; function to verify the request before doing any real work.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;💡 If you haven't set up auth yet, see our guides on &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/how-to-add-clerk-authentication-in-nextjs-16&quot;&gt;Clerk&lt;/a&gt; or &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/how-to-add-nextauth-in-nextjs-16&quot;&gt;Auth.js (NextAuth)&lt;/a&gt; for the full setup.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;wrapping-up&quot;&gt;Wrapping Up&lt;/h3&gt;
&lt;p&gt;Route Handlers are the App Router's answer to &quot;I need a real, callable HTTP endpoint&quot; — whether that's a public API, a Stripe webhook, or a small internal utility route. The mental model is simple once it clicks: a &lt;code&gt;route.ts&lt;/code&gt; file, named exports per HTTP method, and the standard &lt;code&gt;Request&lt;/code&gt;/&lt;code&gt;Response&lt;/code&gt; APIs doing the actual work. Layer in dynamic segments, caching with &lt;code&gt;&quot;use cache&quot;&lt;/code&gt;, and CORS headers as your API grows, and you've got everything you need to build a production-ready backend without leaving your Next.js project.&lt;/p&gt;
&lt;p&gt;From here, a natural next step is pairing your Route Handlers with &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/tools/zod-schema-generator&quot;&gt;Zod validation&lt;/a&gt; for safer request parsing, or wiring one up as a real webhook receiver following the Stripe pattern above.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24571/route-handlers-explained-in-next-js-16-beginners-guide</guid>
<pubDate>Thu, 13 Aug 2026 05:30:25 +0000</pubDate>
</item>
</channel>
</rss>