[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"search-suggestions":44,"article-nextjs-weekly-route-handlers-caching-streaming":93,"related-nextjs-weekly-route-handlers-caching-streaming":593,"code:ts:true:1dabejp":668,"code:ts:true:pusv8m":669,"code:ts:true:y307sd":670,"code:ts:true:1fsj9o5":671,"code:ts:true:1x1k3hp":672,"code:ts:true:1l6rm5e":673,"code:ts:true:16pbxxf":674,"code:ts:true:1bwdz36":675,"comments-01a0ec73-0927-7298-a1ae-88b0d2458ab0":676},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,[45,49,53,57,61,65,69,73,77,81,85,89],{"slug":46,"name":47,"articles":48},"webdev","Webdev",114,{"slug":50,"name":51,"articles":52},"javascript","Javascript",96,{"slug":54,"name":55,"articles":56},"frontend","Frontend",75,{"slug":58,"name":59,"articles":60},"tutorial","Tutorial",41,{"slug":62,"name":63,"articles":64},"css","Css",36,{"slug":66,"name":67,"articles":68},"typescript","Typescript",17,{"slug":70,"name":71,"articles":72},"performance","Performance",14,{"slug":74,"name":75,"articles":76},"react","React",13,{"slug":78,"name":79,"articles":80},"browser","Browser",11,{"slug":82,"name":83,"articles":84},"node","Node",10,{"slug":86,"name":87,"articles":88},"html","Html",8,{"slug":90,"name":91,"articles":92},"accessibility","Accessibility",7,{"id":94,"slug":95,"title":96,"subtitle":97,"excerpt":98,"coverUrl":99,"locale":13,"readingMinutes":100,"publishedAt":101,"viewCount":102,"likeCount":19,"commentCount":19,"author":103,"vertical":108,"topic":109,"tags":112,"_count":117,"playground":119,"body":121,"bodyMd":556,"seo":557,"translationGroupId":559,"series":560,"podcastUrl":97,"verticalId":5,"thread":582,"assessments":584,"translations":589,"quiz":591},"01a0ec73-0927-7298-a1ae-88b0d2458ab0","nextjs-weekly-route-handlers-caching-streaming","Next.js Route Handlers: GET Stopped Caching in 15 — How to Cache in 16",null,"Next.js 15 made GET Route Handlers dynamic by default. What that changes for code written for 14, how to cache one on purpose in 16, plus a cheat sheet.","\u002Fmedia\u002Fcovers\u002Fnextjs-weekly-route-handlers-caching-streaming.png",15,"2026-09-29T09:16:08.176Z",61,{"id":104,"name":105,"username":106,"avatarUrl":97,"headline":107},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":110,"name":111},"nextjs","Nextjs",[113,114,115,116],{"slug":110,"name":111,"color":97},{"slug":66,"name":67,"color":97},{"slug":46,"name":47,"color":97},{"slug":58,"name":59,"color":97},{"assessments":118},1,{"slug":95,"title":120},"Next.js Route Handlers — caching across versions playground",{"blocks":122,"version":118},[123,127,130,135,138,147,150,153,156,159,170,173,176,182,185,188,191,194,200,203,206,209,212,215,242,245,248,251,254,257,261,264,267,270,274,277,281,284,288,291,294,298,301,305,308,311,315,318,322,325,329,332,335,338,341,345,348,351,354,365,368,371,374,377,380,383,386,389,392,395,398,401,404,407,411,414,417,479,482,490,493,496,499,502,505,508,511,514,517,520,523,526,529,532,538,541,544,547,550],{"id":124,"html":125,"type":126},"b1","\u003Cp>Picture a Next.js 14 app whose \u003Ccode>GET \u002Fapi\u002Fproducts\u003C\u002Fcode> handler reads prices from the database. A price drops from $40 to $35, and the API keeps answering $40: Next.js 14 ran the handler once at build time and serves that saved response. Upgrade the same file to Next.js 15 and the stale price is gone, and so is the cache. Every request now runs the query. Same code, opposite behaviour.\u003C\u002Fp>","paragraph",{"id":128,"html":129,"type":126},"b2","\u003Cp>That&#39;s a scenario, not an incident report, but each half follows from its version&#39;s documented default. Next.js Route Handlers switched \u003Ccode>GET\u003C\u002Fcode> from static to dynamic in version 15, and 16 added a second caching model on top. This episode covers what changed, what it means for code and tutorials from the 14 era, and how to cache a Route Handler on purpose today.\u003C\u002Fp>",{"id":131,"html":132,"text":133,"type":134,"level":31},"b3","What you&#39;ll learn","What you'll learn","heading",{"id":136,"html":137,"type":126},"b4","\u003Cp>By the end of this article you&#39;ll be able to:\u003C\u002Fp>",{"id":139,"type":140,"items":141,"ordered":18},"b5","list",[142,143,144,145,146],"Explain what changed for \u003Ccode>GET\u003C\u002Fcode> Route Handlers between Next.js 14 and 15, and why a handler migrated from 14 now runs on every request","Cache a \u003Ccode>GET\u003C\u002Fcode> handler on purpose in Next.js 16: \u003Ccode>force-static\u003C\u002Fcode> and \u003Ccode>revalidate\u003C\u002Fcode> under the default model, a \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> helper under \u003Ccode>cacheComponents\u003C\u002Fcode>","Predict when a \u003Ccode>GET\u003C\u002Fcode> handler is prerendered at build time under \u003Ccode>cacheComponents\u003C\u002Fcode>, and what stops it","Read dynamic segments correctly now that \u003Ccode>params\u003C\u002Fcode> is a Promise and synchronous access is gone","Stream a response, and decide when a Route Handler is the right tool instead of a Server Action or a page",{"id":148,"html":149,"text":149,"type":134,"level":31},"b6","Who this is for",{"id":151,"html":152,"type":126},"b7","\u003Cp>You&#39;ve built at least one App Router route and used \u003Ccode>fetch\u003C\u002Fcode> inside a Server Component. No Pages Router experience is needed.\u003C\u002Fp>",{"id":154,"html":155,"type":126},"b8","\u003Cp>This is written against \u003Cstrong>Next.js 16.3\u003C\u002Fstrong> (npm \u003Ccode>latest\u003C\u002Fcode> is 16.3.6; docs verified September 2026). Next.js 16 has two caching models: the default one, and Cache Components, enabled with \u003Ccode>cacheComponents: true\u003C\u002Fcode> in \u003Ccode>next.config\u003C\u002Fcode>. A fresh \u003Ccode>create-next-app\u003C\u002Fcode> project doesn&#39;t set the flag (its generated config is empty), so this article covers both, plus the Next.js 14 behaviour you&#39;ll still meet in older code and tutorials.\u003C\u002Fp>",{"id":157,"html":158,"text":158,"type":134,"level":31},"b9","Table of contents",{"id":160,"type":140,"items":161,"ordered":18},"b10",[162,163,164,165,166,167,168,169],"\u003Ca href=\"#the-problem-nextjs-route-handlers-changed-their-default-in-15\">The problem: Next.js Route Handlers changed their default in 15\u003C\u002Fa>","\u003Ca href=\"#the-mental-model\">The mental model\u003C\u002Fa>","\u003Ca href=\"#caching-nextjs-route-handlers-on-purpose-in-16\">Caching Next.js Route Handlers on purpose in 16\u003C\u002Fa>","\u003Ca href=\"#edge-cases-and-gotchas\">Edge cases and gotchas\u003C\u002Fa>","\u003Ca href=\"#best-practices-route-handler-server-action-or-page\">Best practices: Route Handler, Server Action, or page?\u003C\u002Fa>","\u003Ca href=\"#faq\">FAQ\u003C\u002Fa>","\u003Ca href=\"#cheat-sheet\">Cheat sheet\u003C\u002Fa>","\u003Ca href=\"#key-takeaways\">Key takeaways\u003C\u002Fa>",{"id":171,"html":172,"text":172,"type":134,"level":31},"b11","The problem: Next.js Route Handlers changed their default in 15",{"id":174,"html":175,"type":126},"b12","\u003Cp>Here&#39;s the handler from the scenario, written the way most people write their first one:\u003C\u002Fp>",{"id":177,"code":178,"type":179,"language":180,"highlight":181},"b13","\u002F\u002F app\u002Fapi\u002Fproducts\u002Froute.ts\nimport { db } from \"@\u002Flib\u002Fdb\";\n\nexport async function GET() {\n  const products = await db.query(\"SELECT id, name, price FROM products\");\n  return Response.json(products);\n}","code","ts",[],{"id":183,"html":184,"type":126},"b14","\u003Cp>\u003Cstrong>On Next.js 14, this is the $40 bug.\u003C\u002Fstrong> The Next.js 14 docs say so directly: &quot;Route Handlers are cached by default when using the \u003Ccode>GET\u003C\u002Fcode> method with the \u003Ccode>Response\u003C\u002Fcode> object.&quot; The ways out were reading the \u003Ccode>Request\u003C\u002Fcode> object, using another HTTP method, calling \u003Ccode>cookies()\u003C\u002Fcode> or \u003Ccode>headers()\u003C\u002Fcode>, or setting a segment config option. This handler does none of those, so Next.js 14 evaluated it during \u003Ccode>next build\u003C\u002Fcode>, and the database stopped mattering until the next deploy.\u003C\u002Fp>",{"id":186,"html":187,"type":126},"b15","\u003Cp>\u003Cstrong>On Next.js 15, the default flipped.\u003C\u002Fstrong> From the Next.js 15 release notes (October 2024): &quot;In Next 14, Route Handlers that used the \u003Ccode>GET\u003C\u002Fcode> HTTP method were cached by default unless they used a dynamic function or dynamic config option. In Next.js 15, \u003Ccode>GET\u003C\u002Fcode> functions are \u003Cstrong>not cached by default\u003C\u002Fstrong>.&quot; The \u003Ccode>route.js\u003C\u002Fcode> reference for 16.3.6 records the same change in its version history: &quot;The default caching for \u003Ccode>GET\u003C\u002Fcode> handlers was changed from static to dynamic&quot; (v15.0.0-RC).\u003C\u002Fp>",{"id":189,"html":190,"type":126},"b16","\u003Cp>So on Next.js 16, under the default model, that handler runs on every request: correct prices, and one query per request where there used to be none.\u003C\u002Fp>",{"id":192,"html":193,"text":193,"type":134,"level":43},"b17","What the change means for code migrated from 14",{"id":195,"type":140,"items":196,"ordered":18},"b18",[197,198,199],"\u003Cstrong>Handlers that were quietly static now run per request.\u003C\u002Fstrong> Correctness improves; load and latency change. If an endpoint \u003Cem>should\u003C\u002Fem> be cached, in 15+ you have to say so.","\u003Cstrong>\u003Ccode>export const dynamic = &quot;force-dynamic&quot;\u003C\u002Fcode> is often a leftover.\u003C\u002Fstrong> The Next.js 14 Route Handlers page opened with exactly that line, so plenty of 14-era files carry it. Under the default model in 16 it&#39;s redundant for a \u003Ccode>GET\u003C\u002Fcode>. Once you enable \u003Ccode>cacheComponents\u003C\u002Fcode>, it breaks: &quot;route segments that still export \u003Ccode>dynamic\u003C\u002Fcode>, \u003Ccode>revalidate\u003C\u002Fcode>, or \u003Ccode>fetchCache\u003C\u002Fcode> will error.&quot;","\u003Cstrong>Tutorials still teach the old default.\u003C\u002Fstrong> Anything written for 14 that says &quot;GET handlers are cached unless…&quot; describes behaviour that ended in 15. The fix for 14 (opt \u003Cem>out\u003C\u002Fem>) is the opposite of the fix for 16 (opt \u003Cem>in\u003C\u002Fem>), so check the version before you copy one.",{"id":201,"html":202,"text":202,"type":134,"level":31},"b19","The mental model",{"id":204,"html":205,"type":126},"b20","\u003Cp>\u003Cstrong>In Next.js 16, a \u003Ccode>GET\u003C\u002Fcode> Route Handler runs on every request unless something states otherwise, and what counts as &quot;stating otherwise&quot; depends on which caching model you&#39;re on.\u003C\u002Fstrong>\u003C\u002Fp>",{"id":207,"html":208,"type":126},"b21","\u003Cp>Under the \u003Cstrong>default model\u003C\u002Fstrong>, the statement is a segment config line. The docs: &quot;Route Handlers are not cached by default. You can, however, opt into caching for \u003Ccode>GET\u003C\u002Fcode> methods,&quot; with \u003Ccode>export const dynamic = &#39;force-static&#39;\u003C\u002Fcode>.\u003C\u002Fp>",{"id":210,"html":211,"type":126},"b22","\u003Cp>Under \u003Cstrong>Cache Components\u003C\u002Fstrong>, the statement is the code itself: &quot;\u003Ccode>GET\u003C\u002Fcode> Route Handlers follow the same model as normal UI routes in your application. They run at request time by default, can be prerendered when they don&#39;t access uncached or runtime data, and you can use \u003Ccode>use cache\u003C\u002Fcode> to include uncached data in the static response.&quot;\u003C\u002Fp>",{"id":213,"html":214,"type":126},"b23","\u003Cp>Side by side:\u003C\u002Fp>",{"id":216,"head":217,"rows":222,"type":241},"b24",[218,219,220,221],"","Next.js 14","Next.js 15\u002F16, default model","Next.js 16, \u003Ccode>cacheComponents: true\u003C\u002Fcode>",[223,228,233,238],[224,225,226,227],"\u003Ccode>GET\u003C\u002Fcode> with no dynamic input","Evaluated at build time, cached","Runs on every request","Prerendered only if it touches no uncached or runtime data",[229,230,231,232],"The scenario&#39;s DB query","Cached (the $40 bug)","Runs per request","Runs per request (a DB query stops prerendering)",[234,235,236,237],"Cache it on purpose","Already cached; \u003Ccode>revalidate\u003C\u002Fcode> for a window","\u003Ccode>export const dynamic = &quot;force-static&quot;\u003C\u002Fcode>","A \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> helper with \u003Ccode>cacheLife\u003C\u002Fcode>",[239,240,240,240],"Non-\u003Ccode>GET\u003C\u002Fcode> methods","Never cached","table",{"id":243,"html":244,"type":126},"b25","\u003Cp>The last row never changes: &quot;Other supported HTTP methods are \u003Cstrong>not\u003C\u002Fstrong> cached, even if they are placed alongside a \u003Ccode>GET\u003C\u002Fcode> method that is cached, in the same file.&quot;\u003C\u002Fp>",{"id":246,"html":247,"type":126},"b26","\u003Cp>Hold on to one inversion and the rest of this article follows: \u003Cstrong>in 14 you wrote config to get out of the cache; in 16 you write config, or \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode>, to get in.\u003C\u002Fstrong>\u003C\u002Fp>",{"id":249,"html":250,"text":250,"type":134,"level":31},"b27","Caching Next.js Route Handlers on purpose in 16",{"id":252,"html":253,"text":253,"type":134,"level":43},"b28","The file convention",{"id":255,"html":256,"type":126},"b29","\u003Cp>A Route Handler lives in a \u003Ccode>route.ts\u003C\u002Fcode> (or \u003Ccode>.js\u003C\u002Fcode>) file under \u003Ccode>app\u002F\u003C\u002Fcode> and exports one async function per HTTP method: \u003Ccode>GET\u003C\u002Fcode>, \u003Ccode>POST\u003C\u002Fcode>, \u003Ccode>PUT\u003C\u002Fcode>, \u003Ccode>PATCH\u003C\u002Fcode>, \u003Ccode>DELETE\u003C\u002Fcode>, \u003Ccode>HEAD\u003C\u002Fcode>, \u003Ccode>OPTIONS\u003C\u002Fcode>. Next.js reads the exported names; there&#39;s no router table. A method you didn&#39;t export gets \u003Ccode>405 Method Not Allowed\u003C\u002Fcode>, and if you don&#39;t export \u003Ccode>OPTIONS\u003C\u002Fcode>, Next.js implements it and sets the \u003Ccode>Allow\u003C\u002Fcode> header for you.\u003C\u002Fp>",{"id":258,"code":259,"type":179,"language":180,"highlight":260},"b30","\u002F\u002F app\u002Fapi\u002Fproducts\u002Froute.ts\nexport async function GET(request: Request) {\n  return Response.json({ ok: true });\n}\n\nexport async function POST(request: Request) {\n  const body = await request.json();\n  return Response.json({ received: body }, { status: 201 });\n}",[],{"id":262,"html":263,"type":126},"b31","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> a \u003Ccode>route.ts\u003C\u002Fcode> and a \u003Ccode>page.tsx\u003C\u002Fcode> can&#39;t share a route segment. \u003Ccode>app\u002Fpage.js\u003C\u002Fcode> plus \u003Ccode>app\u002Froute.js\u003C\u002Fcode> is a conflict; \u003Ccode>app\u002Fpage.js\u003C\u002Fcode> plus \u003Ccode>app\u002Fapi\u002Froute.js\u003C\u002Fcode> is fine.\u003C\u002Fp>",{"id":265,"html":266,"text":266,"type":134,"level":43},"b32","Request and response",{"id":268,"html":269,"type":126},"b33","\u003Cp>Plain Web \u003Ccode>Request\u003C\u002Fcode>\u002F\u003Ccode>Response\u003C\u002Fcode> are enough for a JSON API. The \u003Ccode>request\u003C\u002Fcode> argument is actually a \u003Ccode>NextRequest\u003C\u002Fcode>, which adds \u003Ccode>request.nextUrl\u003C\u002Fcode> (a parsed URL) and \u003Ccode>request.cookies\u003C\u002Fcode>.\u003C\u002Fp>",{"id":271,"code":272,"type":179,"language":180,"highlight":273},"b34","import type { NextRequest } from \"next\u002Fserver\";\n\nexport async function GET(request: NextRequest) {\n  const query = request.nextUrl.searchParams.get(\"q\");\n  return Response.json({ query });\n}",[],{"id":275,"html":276,"type":126},"b35","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> reading the request changes nothing under the default model, because the handler already runs per request. Under \u003Ccode>cacheComponents\u003C\u002Fcode> it matters: &quot;request object properties (like \u003Ccode>req.url\u003C\u002Fcode>, \u003Ccode>request.headers\u003C\u002Fcode>, \u003Ccode>request.cookies\u003C\u002Fcode>, \u003Ccode>request.body\u003C\u002Fcode>)&quot; are on the docs&#39; list of things that stop a \u003Ccode>GET\u003C\u002Fcode> handler from prerendering.\u003C\u002Fp>",{"id":278,"html":279,"text":280,"type":134,"level":43},"b36","Dynamic segments: \u003Ccode>params\u003C\u002Fcode> is a Promise","Dynamic segments: params is a Promise",{"id":282,"html":283,"type":126},"b37","\u003Cp>For \u003Ccode>app\u002Fapi\u002Fproducts\u002F[id]\u002Froute.ts\u003C\u002Fcode>, the handler&#39;s second argument carries \u003Ccode>params\u003C\u002Fcode>, and since Next.js 15 it&#39;s a Promise you \u003Ccode>await\u003C\u002Fcode>:\u003C\u002Fp>",{"id":285,"code":286,"type":179,"language":180,"highlight":287},"b38","\u002F\u002F app\u002Fapi\u002Fproducts\u002F[id]\u002Froute.ts\nimport { db } from \"@\u002Flib\u002Fdb\";\n\nexport async function GET(\n  request: Request,\n  ctx: RouteContext\u003C\"\u002Fapi\u002Fproducts\u002F[id]\">\n) {\n  const { id } = await ctx.params;\n  const product = await db.get(id);\n  if (!product) {\n    return Response.json({ error: \"not found\" }, { status: 404 });\n  }\n  return Response.json(product);\n}",[],{"id":289,"html":290,"type":126},"b39","\u003Cp>\u003Ccode>RouteContext\u003C\u002Fcode> is a global type helper generated by \u003Ccode>next dev\u003C\u002Fcode>, \u003Ccode>next build\u003C\u002Fcode> or \u003Ccode>next typegen\u003C\u002Fcode>; the hand-written \u003Ccode>{ params }: { params: Promise&lt;{ id: string }&gt; }\u003C\u002Fcode> is equivalent.\u003C\u002Fp>",{"id":292,"html":293,"type":126},"b40","\u003Cp>Next.js 15 made \u003Ccode>params\u003C\u002Fcode>, \u003Ccode>cookies()\u003C\u002Fcode> and \u003Ccode>headers()\u003C\u002Fcode> async with a grace period: &quot;these APIs can temporarily be accessed synchronously, but will show warnings in development and production until the next major version.&quot; That major was 16: &quot;synchronous access is fully removed.&quot; If your upgrade skipped the migration, run the codemod: \u003Ccode>npx @next\u002Fcodemod@canary next-async-request-api .\u003C\u002Fcode>\u003C\u002Fp>",{"id":295,"html":296,"text":297,"type":134,"level":43},"b41","Default model: opt in with \u003Ccode>force-static\u003C\u002Fcode>","Default model: opt in with force-static",{"id":299,"html":300,"type":126},"b42","\u003Cp>This replaces &quot;do nothing&quot; from 14. The docs: &quot;To cache a \u003Ccode>GET\u003C\u002Fcode> method, use a route config option such as \u003Ccode>export const dynamic = &#39;force-static&#39;\u003C\u002Fcode> in your Route Handler file.&quot;\u003C\u002Fp>",{"id":302,"code":303,"type":179,"language":180,"highlight":304},"b43","\u002F\u002F app\u002Fapi\u002Fproducts\u002Froute.ts\nimport { db } from \"@\u002Flib\u002Fdb\";\n\nexport const dynamic = \"force-static\"; \u002F\u002F evaluate once, cache the response\nexport const revalidate = 3600; \u002F\u002F optional: refresh at most once an hour (seconds)\n\nexport async function GET() {\n  const products = await db.query(\"SELECT id, name, price FROM products\");\n  return Response.json(products);\n}",[],{"id":306,"html":307,"type":126},"b44","\u003Cp>Without \u003Ccode>revalidate\u003C\u002Fcode>, you&#39;ve recreated the Next.js 14 behaviour on purpose: one evaluation, reused until you redeploy or invalidate it. For on-demand refresh, call \u003Ccode>revalidatePath(&quot;\u002Fapi\u002Fproducts&quot;)\u003C\u002Fcode> from the handler that writes; its reference lists Route Handler paths among what it can invalidate.\u003C\u002Fp>",{"id":309,"html":310,"type":126},"b45","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> \u003Ccode>force-static\u003C\u002Fcode> works by &quot;forcing \u003Ccode>cookies\u003C\u002Fcode>, \u003Ccode>headers()\u003C\u002Fcode> and \u003Ccode>useSearchParams()\u003C\u002Fcode> to return empty values,&quot; so a \u003Ccode>force-static\u003C\u002Fcode> handler that reads a cookie silently sees none. Reserve it for responses that are the same for every caller.\u003C\u002Fp>",{"id":312,"html":313,"text":314,"type":134,"level":43},"b46","Cache Components: \u003Ccode>GET\u003C\u002Fcode> handlers prerender like pages","Cache Components: GET handlers prerender like pages",{"id":316,"html":317,"type":126},"b47","\u003Cp>Enable the flag and the segment configs go away: they &quot;are replaced by \u003Ccode>use cache\u003C\u002Fcode> and \u003Ccode>cacheLife\u003C\u002Fcode>.&quot; A \u003Ccode>GET\u003C\u002Fcode> handler is now judged by what it touches, like a page (the model from \u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-cache-components-explained\">Next.js Cache Components Explained\u003C\u002Fa>).\u003C\u002Fp>",{"id":319,"code":320,"type":179,"language":180,"highlight":321},"b48","\u002F\u002F Prerendered at build time: no uncached or runtime data.\nexport async function GET() {\n  return Response.json({ projectName: \"Next.js\" });\n}",[],{"id":323,"html":324,"type":126},"b49","\u003Cp>The scenario&#39;s handler, with its async database query, does \u003Cem>not\u003C\u002Fem> prerender. Prerendering stops on &quot;network requests, database queries, async file system operations, request object properties (…), runtime APIs like \u003Ccode>cookies()\u003C\u002Fcode>, \u003Ccode>headers()\u003C\u002Fcode>, \u003Ccode>connection()\u003C\u002Fcode>, or non-deterministic operations.&quot; To cache the query, move it into a helper marked \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode>:\u003C\u002Fp>",{"id":326,"code":327,"type":179,"language":180,"highlight":328},"b50","\u002F\u002F app\u002Fapi\u002Fproducts\u002Froute.ts\nimport { cacheLife, cacheTag } from \"next\u002Fcache\";\nimport { db } from \"@\u002Flib\u002Fdb\";\n\nasync function getProducts() {\n  \"use cache\";\n  cacheLife(\"hours\");\n  cacheTag(\"products\");\n  return db.query(\"SELECT id, name, price FROM products\");\n}\n\nexport async function GET() {\n  const products = await getProducts();\n  return Response.json(products);\n}",[],{"id":330,"html":331,"type":126},"b51","\u003Cp>Two rules from the docs: &quot;\u003Ccode>use cache\u003C\u002Fcode> cannot be used directly inside a Route Handler body; extract it to a helper function,&quot; and &quot;Cached responses revalidate according to \u003Ccode>cacheLife\u003C\u002Fcode> when a new request arrives.&quot; The \u003Ccode>cacheTag\u003C\u002Fcode> line lets a write elsewhere invalidate it.\u003C\u002Fp>",{"id":333,"html":334,"type":126},"b52","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> this is &quot;cache the piece, not the route,&quot; the idea Cache Components brought to pages. In a Route Handler, the piece is a helper function instead of a component.\u003C\u002Fp>",{"id":336,"html":337,"text":337,"type":134,"level":43},"b53","Streaming a response",{"id":339,"html":340,"type":126},"b54","\u003Cp>A Route Handler can stream its body instead of buffering it, which suits server-sent events, long exports, or proxying a slow upstream:\u003C\u002Fp>",{"id":342,"code":343,"type":179,"language":180,"highlight":344},"b55","\u002F\u002F app\u002Fapi\u002Fticks\u002Froute.ts\nimport { connection } from \"next\u002Fserver\";\n\nexport async function GET() {\n  await connection(); \u002F\u002F under cacheComponents: explicitly request-time\n  const encoder = new TextEncoder();\n  const stream = new ReadableStream({\n    async start(controller) {\n      for (let i = 0; i \u003C 5; i++) {\n        controller.enqueue(encoder.encode(`event: tick\\ndata: ${i}\\n\\n`));\n        await new Promise((r) => setTimeout(r, 1000));\n      }\n      controller.close();\n    },\n  });\n\n  return new Response(stream, {\n    headers: { \"Content-Type\": \"text\u002Fevent-stream\" },\n  });\n}",[],{"id":346,"html":347,"type":126},"b56","\u003Cp>Under the default model the \u003Ccode>connection()\u003C\u002Fcode> line is optional; the handler already runs per request. Under \u003Ccode>cacheComponents\u003C\u002Fcode> it states the intent, since \u003Ccode>connection()\u003C\u002Fcode> is on the docs&#39; list of calls that stop prerendering.\u003C\u002Fp>",{"id":349,"html":350,"type":126},"b57","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> this is unrelated to a page&#39;s \u003Ccode>&lt;Suspense&gt;\u003C\u002Fcode> streaming. Pages stream \u003Cem>rendered HTML\u003C\u002Fem>; a Route Handler streams \u003Cem>whatever bytes you enqueue\u003C\u002Fem>.\u003C\u002Fp>",{"id":352,"html":353,"text":353,"type":134,"level":31},"b58","Edge cases and gotchas",{"id":355,"type":140,"items":356,"ordered":18},"b59",[357,358,359,360,361,362,363,364],"\u003Cstrong>Non-\u003Ccode>GET\u003C\u002Fcode> methods are never cached, in any version or model.\u003C\u002Fstrong> A cached \u003Ccode>GET\u003C\u002Fcode> doesn&#39;t make the \u003Ccode>POST\u003C\u002Fcode> beside it cached, and there&#39;s no config that does.","\u003Cstrong>A cached \u003Ccode>GET\u003C\u002Fcode> doesn&#39;t know about writes from elsewhere.\u003C\u002Fstrong> Invalidate from the \u003Ccode>POST\u003C\u002Fcode> handler or webhook that writes. Under \u003Ccode>cacheComponents\u003C\u002Fcode>, call \u003Ccode>revalidateTag(&quot;products&quot;, &quot;max&quot;)\u003C\u002Fcode> (the single-argument form is deprecated in 16). Not \u003Ccode>updateTag\u003C\u002Fcode>: it &quot;can only be called from a Server Action; calling it elsewhere throws.&quot; Under the default model, use \u003Ccode>revalidatePath(&quot;\u002Fapi\u002Fproducts&quot;)\u003C\u002Fcode>.","\u003Cstrong>A synchronous database driver can bring the 14-era bug back under \u003Ccode>cacheComponents\u003C\u002Fcode>.\u003C\u002Fstrong> The caching guide says queries to &quot;embedded databases with synchronous APIs, such as \u003Ccode>better-sqlite3\u003C\u002Fcode> or Node.js&#39;s built-in \u003Ccode>node:sqlite\u003C\u002Fcode>&quot; complete during prerendering, and \u003Ccode>GET\u003C\u002Fcode> handlers follow the same model. If the answer must be live, \u003Ccode>await connection()\u003C\u002Fcode> before the query.","\u003Cstrong>\u003Ccode>try\u002Fcatch\u003C\u002Fcode> catches the prerender bail-out.\u003C\u002Fstrong> Under \u003Ccode>cacheComponents\u003C\u002Fcode>, reading uncached or runtime data &quot;bails out of prerendering by \u003Cstrong>throwing\u003C\u002Fstrong>,&quot; so a \u003Ccode>try\u002Fcatch\u003C\u002Fcode> that logs adds noise to the build output (the docs point to \u003Ccode>experimental.hideLogsAfterAbort: true\u003C\u002Fcode>).","\u003Cstrong>Metadata routes kept the old default.\u003C\u002Fstrong> &quot;Special Route Handlers like \u003Ccode>sitemap.ts\u003C\u002Fcode>, \u003Ccode>opengraph-image.tsx\u003C\u002Fcode>, and \u003Ccode>icon.tsx\u003C\u002Fcode>, and other metadata files remain static by default unless they use Request-time APIs or dynamic config options.&quot;","\u003Cstrong>No layouts, no \u003Ccode>error.tsx\u003C\u002Fcode>.\u003C\u002Fstrong> Route Handlers aren&#39;t part of the React tree; handle failures with \u003Ccode>try\u002Fcatch\u003C\u002Fcode> and explicit status codes.","\u003Cstrong>CORS headers are manual.\u003C\u002Fstrong> The automatic \u003Ccode>OPTIONS\u003C\u002Fcode> response sets \u003Ccode>Allow\u003C\u002Fcode>, not \u003Ccode>Access-Control-*\u003C\u002Fcode>. Set them on your responses, or for many handlers at once in \u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-proxyts-explained-with-cheat-sheet\">\u003Ccode>proxy.ts\u003C\u002Fcode>\u003C\u002Fa> or \u003Ccode>next.config\u003C\u002Fcode> headers.","\u003Cstrong>\u003Ccode>export const runtime = &quot;edge&quot;\u003C\u002Fcode> is deprecated.\u003C\u002Fstrong> &quot;The Edge Runtime is deprecated. Remove the \u003Ccode>runtime\u003C\u002Fcode> export from your route files.&quot; Node.js is the default, and Cache Components requires it.",{"id":366,"html":367,"text":367,"type":134,"level":31},"b60","Best practices: Route Handler, Server Action, or page?",{"id":369,"html":370,"type":126},"b61","\u003Cp>Reach for a \u003Cstrong>Route Handler\u003C\u002Fstrong> when the caller isn&#39;t your own App Router UI: a Stripe or GitHub webhook, an OAuth callback, a public API, or a response that isn&#39;t HTML, such as a file download, an RSS feed or an SSE stream.\u003C\u002Fp>",{"id":372,"html":373,"type":126},"b62","\u003Cp>Reach for a \u003Cstrong>Server Action\u003C\u002Fstrong>, covered in \u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-server-actions-mutations-security-cheat-sheet\">Server Actions, Mutations &amp; Security\u003C\u002Fa>, when a form or button in your own UI mutates data. It&#39;s also the only place \u003Ccode>updateTag\u003C\u002Fcode>&#39;s read-your-own-writes refresh works.\u003C\u002Fp>",{"id":375,"html":376,"type":126},"b63","\u003Cp>Reach for a \u003Cstrong>page&#39;s own data fetching\u003C\u002Fstrong> when nothing outside your app needs the data. A Route Handler that exists only so your own page can \u003Ccode>fetch()\u003C\u002Fcode> it is usually one hop you don&#39;t need.\u003C\u002Fp>",{"id":378,"html":379,"type":126},"b64","\u003Cp>Whichever you pick, \u003Cstrong>write the caching decision into the file.\u003C\u002Fstrong> A reviewer can see a \u003Ccode>force-static\u003C\u002Fcode> line or a \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> helper. A default is something they have to know, and this one has already changed once.\u003C\u002Fp>",{"id":381,"html":382,"text":382,"type":134,"level":31},"b65","FAQ",{"id":384,"html":385,"text":385,"type":134,"level":43},"b66","Are Next.js Route Handlers cached by default?",{"id":387,"html":388,"type":126},"b67","\u003Cp>No, not since Next.js 15. Under the default model, a \u003Ccode>GET\u003C\u002Fcode> handler runs on every request until you add \u003Ccode>export const dynamic = &quot;force-static&quot;\u003C\u002Fcode>; other methods are never cached. Under \u003Ccode>cacheComponents\u003C\u002Fcode>, a \u003Ccode>GET\u003C\u002Fcode> handler is prerendered only if it touches no uncached or runtime data.\u003C\u002Fp>",{"id":390,"html":391,"text":391,"type":134,"level":43},"b68","Why did my GET Route Handler stop being cached after upgrading to Next.js 15?",{"id":393,"html":394,"type":126},"b69","\u003Cp>Because 15 changed the default from static to dynamic; in 14 it was cached only because nothing opted it out. Add \u003Ccode>export const dynamic = &quot;force-static&quot;\u003C\u002Fcode> (plus \u003Ccode>revalidate\u003C\u002Fcode> for a refresh window), or a \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> helper under \u003Ccode>cacheComponents\u003C\u002Fcode>.\u003C\u002Fp>",{"id":396,"html":397,"text":397,"type":134,"level":43},"b70","How do I cache a Route Handler when cacheComponents is enabled?",{"id":399,"html":400,"type":126},"b71","\u003Cp>Move the data access into a helper marked \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode>, give it a \u003Ccode>cacheLife\u003C\u002Fcode> (and a \u003Ccode>cacheTag\u003C\u002Fcode> if writes should invalidate it), and call it from the handler. The directive can&#39;t go in the handler body, and \u003Ccode>dynamic\u003C\u002Fcode>, \u003Ccode>revalidate\u003C\u002Fcode> and \u003Ccode>fetchCache\u003C\u002Fcode> exports error under the flag.\u003C\u002Fp>",{"id":402,"html":403,"text":403,"type":134,"level":43},"b72","Can I use cookies() or headers() inside a Route Handler?",{"id":405,"html":406,"type":126},"b73","\u003Cp>Yes. Import them from \u003Ccode>next\u002Fheaders\u003C\u002Fcode> and \u003Ccode>await\u003C\u002Fcode> them; both are async since 15, and synchronous access is removed in 16. Under \u003Ccode>force-static\u003C\u002Fcode> they return empty values, and under \u003Ccode>cacheComponents\u003C\u002Fcode> calling them keeps the handler at request time.\u003C\u002Fp>",{"id":408,"html":409,"text":410,"type":134,"level":43},"b74","Why does my dynamic segment&#39;s params need an await?","Why does my dynamic segment's params need an await?",{"id":412,"html":413,"type":126},"b75","\u003Cp>Next.js 15 made the request-time APIs (\u003Ccode>params\u003C\u002Fcode>, \u003Ccode>searchParams\u003C\u002Fcode>, \u003Ccode>cookies()\u003C\u002Fcode>, \u003Ccode>headers()\u003C\u002Fcode>, \u003Ccode>draftMode()\u003C\u002Fcode>) asynchronous, so the framework knows when work has to wait for a request. 15 allowed synchronous access with warnings; 16 removed it.\u003C\u002Fp>",{"id":415,"html":416,"text":416,"type":134,"level":31},"b76","Cheat sheet",{"id":418,"head":419,"rows":423,"type":241},"b77",[420,421,422],"Task","Code","Notes",[424,428,432,435,439,443,447,451,455,459,463,467,471,475],[425,426,427],"Define a handler","\u003Ccode>export async function GET(req: Request) {}\u003C\u002Fcode>","One export per method in \u003Ccode>route.ts\u003C\u002Fcode>; unexported methods get 405",[429,430,431],"Default for \u003Ccode>GET\u003C\u002Fcode> (15+, default model)","nothing to write","Runs on every request; was static in 14",[433,236,434],"Cache a \u003Ccode>GET\u003C\u002Fcode> (default model)","The documented opt-in since 15",[436,437,438],"Add a refresh window (default model)","\u003Ccode>export const revalidate = 3600\u003C\u002Fcode>","Seconds; pair with \u003Ccode>force-static\u003C\u002Fcode>",[440,441,442],"Refresh on demand (default model)","\u003Ccode>revalidatePath(&quot;\u002Fapi\u002Fproducts&quot;)\u003C\u002Fcode>","Call from the handler that writes",[444,445,446],"Cache under \u003Ccode>cacheComponents\u003C\u002Fcode>","\u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> + \u003Ccode>cacheLife(&quot;hours&quot;)\u003C\u002Fcode> in a helper","Not in the handler body; segment configs error",[448,449,450],"Invalidate under \u003Ccode>cacheComponents\u003C\u002Fcode>","\u003Ccode>cacheTag(&quot;products&quot;)\u003C\u002Fcode> → \u003Ccode>revalidateTag(&quot;products&quot;, &quot;max&quot;)\u003C\u002Fcode>","\u003Ccode>updateTag\u003C\u002Fcode> is Server Actions only",[452,453,454],"Force request time (\u003Ccode>cacheComponents\u003C\u002Fcode>)","\u003Ccode>await connection()\u003C\u002Fcode>","From \u003Ccode>next\u002Fserver\u003C\u002Fcode>",[456,457,458],"Read a dynamic segment","\u003Ccode>const { id } = await ctx.params\u003C\u002Fcode>","Promise since 15; sync access removed in 16",[460,461,462],"Type the context","\u003Ccode>ctx: RouteContext&lt;&quot;\u002Fapi\u002Fproducts\u002F[id]&quot;&gt;\u003C\u002Fcode>","Generated by \u003Ccode>next dev\u003C\u002Fcode> \u002F \u003Ccode>build\u003C\u002Fcode> \u002F \u003Ccode>typegen\u003C\u002Fcode>",[464,465,466],"Read a query string","\u003Ccode>request.nextUrl.searchParams.get(&quot;q&quot;)\u003C\u002Fcode>","Stops prerendering under \u003Ccode>cacheComponents\u003C\u002Fcode>",[468,469,470],"Read a cookie","\u003Ccode>(await cookies()).get(&quot;name&quot;)\u003C\u002Fcode>","From \u003Ccode>next\u002Fheaders\u003C\u002Fcode>; empty under \u003Ccode>force-static\u003C\u002Fcode>",[472,473,474],"Stream a response","\u003Ccode>new Response(new ReadableStream({ ... }))\u003C\u002Fcode>","SSE, exports, proxying",[476,477,478],"Runtime","delete \u003Ccode>export const runtime = &quot;edge&quot;\u003C\u002Fcode>","Edge is deprecated; Node.js is the default",{"id":480,"html":481,"text":481,"type":134,"level":31},"b78","Key takeaways",{"id":483,"type":140,"items":484,"ordered":18},"b79",[485,486,487,488,489],"Next.js 15 flipped \u003Ccode>GET\u003C\u002Fcode> Route Handlers from cached to dynamic by default. Next.js 14 code and tutorials describe the opposite default.","In 14 you wrote config to opt \u003Cem>out\u003C\u002Fem> of the cache; in 16 you write it to opt \u003Cem>in\u003C\u002Fem>: \u003Ccode>force-static\u003C\u002Fcode> under the default model, a \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> helper under \u003Ccode>cacheComponents\u003C\u002Fcode>.","Under \u003Ccode>cacheComponents\u003C\u002Fcode>, \u003Ccode>GET\u003C\u002Fcode> handlers follow the page rule: prerendered unless they touch uncached or runtime data, and \u003Ccode>dynamic\u003C\u002Fcode>\u002F\u003Ccode>revalidate\u003C\u002Fcode>\u002F\u003Ccode>fetchCache\u003C\u002Fcode> exports error.","\u003Ccode>params\u003C\u002Fcode> is a Promise; 15 tolerated synchronous access, 16 removed it.","Non-\u003Ccode>GET\u003C\u002Fcode> methods are never cached. Choose a Route Handler for callers outside your own UI and a Server Action for your own forms.",{"id":491,"html":492,"type":126},"b80","\u003Cp>The $40 answer and the query-on-every-request are the same missing decision, seen from two versions. In 14 the framework decided &quot;cache it&quot;; in 15 it decided &quot;don&#39;t&quot;. Either way the handler had a caching policy that nobody wrote down. Put the \u003Ccode>force-static\u003C\u002Fcode>, \u003Ccode>revalidate\u003C\u002Fcode> or \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> line in the file yourself, and the next change of default can&#39;t surprise you.\u003C\u002Fp>",{"id":494,"html":495,"type":126},"b81","\u003Cp>Which Route Handler in your app is still relying on a default it inherited from 14? Tell me in the comments.\u003C\u002Fp>",{"id":497,"html":498,"type":126},"b82","\u003C!-- playground:start -->",{"id":500,"html":501,"text":501,"type":134,"level":31},"b83","🎮 Try it yourself",{"id":503,"html":504,"type":126},"b84","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-route-handlers-caching-streaming\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":506,"html":507,"type":126},"b85","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":509,"html":510,"type":126},"b86","\u003C!-- playground:end -->",{"id":512,"html":513,"type":126},"b87","\u003C!-- quiz:start -->",{"id":515,"html":516,"text":516,"type":134,"level":31},"b88","🧠 Test yourself",{"id":518,"html":519,"type":126},"b89","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-route-handlers-caching-streaming\u002Fquiz\">Take the 8-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":521,"html":522,"type":126},"b90","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":524,"html":525,"type":126},"b91","\u003C!-- quiz:end -->",{"id":527,"html":528,"type":126},"b92","\u003C!-- related:start -->",{"id":530,"html":531,"text":531,"type":134,"level":31},"b93","📚 Read next",{"id":533,"type":140,"items":534,"ordered":18},"b94",[535,536,537],"\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fserver-sent-events-eventsource-live-updates\">You Don&#39;t Need a WebSocket for That Live Feed\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Ffetch-already-streams-readablestream\">Your Fetch Already Streams. You&#39;re Buffering It Anyway.\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-middleware-to-proxy-network-boundary\">Next.js proxy.ts Explained (with Cheat Sheet)\u003C\u002Fa>",{"id":539,"html":540,"type":126},"b95","\u003C!-- related:end -->",{"id":542,"type":543},"b96","divider",{"id":545,"html":546,"type":126},"b97","\u003Cp>🚀 \u003Cstrong>Want more like this?\u003C\u002Fstrong> Every guide, playground, and quiz lives on \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">bestpractic.org\u003C\u002Fa>\u003C\u002Fstrong> — open it and \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">sign up free\u003C\u002Fa>\u003C\u002Fstrong> so the next one finds you.\u003C\u002Fp>",{"id":548,"html":549,"type":126},"b98","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":551,"type":140,"items":552,"ordered":18},"b99",[553,554,555],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","Picture a Next.js 14 app whose `GET \u002Fapi\u002Fproducts` handler reads prices from the database. A price drops from $40 to $35, and the API keeps answering $40: Next.js 14 ran the handler once at build time and serves that saved response. Upgrade the same file to Next.js 15 and the stale price is gone, and so is the cache. Every request now runs the query. Same code, opposite behaviour.\n\nThat's a scenario, not an incident report, but each half follows from its version's documented default. Next.js Route Handlers switched `GET` from static to dynamic in version 15, and 16 added a second caching model on top. This episode covers what changed, what it means for code and tutorials from the 14 era, and how to cache a Route Handler on purpose today.\n\n## What you'll learn\n\nBy the end of this article you'll be able to:\n\n- Explain what changed for `GET` Route Handlers between Next.js 14 and 15, and why a handler migrated from 14 now runs on every request\n- Cache a `GET` handler on purpose in Next.js 16: `force-static` and `revalidate` under the default model, a `\"use cache\"` helper under `cacheComponents`\n- Predict when a `GET` handler is prerendered at build time under `cacheComponents`, and what stops it\n- Read dynamic segments correctly now that `params` is a Promise and synchronous access is gone\n- Stream a response, and decide when a Route Handler is the right tool instead of a Server Action or a page\n\n## Who this is for\n\nYou've built at least one App Router route and used `fetch` inside a Server Component. No Pages Router experience is needed.\n\nThis is written against **Next.js 16.3** (npm `latest` is 16.3.6; docs verified September 2026). Next.js 16 has two caching models: the default one, and Cache Components, enabled with `cacheComponents: true` in `next.config`. A fresh `create-next-app` project doesn't set the flag (its generated config is empty), so this article covers both, plus the Next.js 14 behaviour you'll still meet in older code and tutorials.\n\n## Table of contents\n\n- [The problem: Next.js Route Handlers changed their default in 15](#the-problem-nextjs-route-handlers-changed-their-default-in-15)\n- [The mental model](#the-mental-model)\n- [Caching Next.js Route Handlers on purpose in 16](#caching-nextjs-route-handlers-on-purpose-in-16)\n- [Edge cases and gotchas](#edge-cases-and-gotchas)\n- [Best practices: Route Handler, Server Action, or page?](#best-practices-route-handler-server-action-or-page)\n- [FAQ](#faq)\n- [Cheat sheet](#cheat-sheet)\n- [Key takeaways](#key-takeaways)\n\n## The problem: Next.js Route Handlers changed their default in 15\n\nHere's the handler from the scenario, written the way most people write their first one:\n\n```ts\n\u002F\u002F app\u002Fapi\u002Fproducts\u002Froute.ts\nimport { db } from \"@\u002Flib\u002Fdb\";\n\nexport async function GET() {\n  const products = await db.query(\"SELECT id, name, price FROM products\");\n  return Response.json(products);\n}\n```\n\n**On Next.js 14, this is the $40 bug.** The Next.js 14 docs say so directly: \"Route Handlers are cached by default when using the `GET` method with the `Response` object.\" The ways out were reading the `Request` object, using another HTTP method, calling `cookies()` or `headers()`, or setting a segment config option. This handler does none of those, so Next.js 14 evaluated it during `next build`, and the database stopped mattering until the next deploy.\n\n**On Next.js 15, the default flipped.** From the Next.js 15 release notes (October 2024): \"In Next 14, Route Handlers that used the `GET` HTTP method were cached by default unless they used a dynamic function or dynamic config option. In Next.js 15, `GET` functions are **not cached by default**.\" The `route.js` reference for 16.3.6 records the same change in its version history: \"The default caching for `GET` handlers was changed from static to dynamic\" (v15.0.0-RC).\n\nSo on Next.js 16, under the default model, that handler runs on every request: correct prices, and one query per request where there used to be none.\n\n### What the change means for code migrated from 14\n\n- **Handlers that were quietly static now run per request.** Correctness improves; load and latency change. If an endpoint *should* be cached, in 15+ you have to say so.\n- **`export const dynamic = \"force-dynamic\"` is often a leftover.** The Next.js 14 Route Handlers page opened with exactly that line, so plenty of 14-era files carry it. Under the default model in 16 it's redundant for a `GET`. Once you enable `cacheComponents`, it breaks: \"route segments that still export `dynamic`, `revalidate`, or `fetchCache` will error.\"\n- **Tutorials still teach the old default.** Anything written for 14 that says \"GET handlers are cached unless…\" describes behaviour that ended in 15. The fix for 14 (opt *out*) is the opposite of the fix for 16 (opt *in*), so check the version before you copy one.\n\n## The mental model\n\n**In Next.js 16, a `GET` Route Handler runs on every request unless something states otherwise, and what counts as \"stating otherwise\" depends on which caching model you're on.**\n\nUnder the **default model**, the statement is a segment config line. The docs: \"Route Handlers are not cached by default. You can, however, opt into caching for `GET` methods,\" with `export const dynamic = 'force-static'`.\n\nUnder **Cache Components**, the statement is the code itself: \"`GET` Route Handlers follow the same model as normal UI routes in your application. They run at request time by default, can be prerendered when they don't access uncached or runtime data, and you can use `use cache` to include uncached data in the static response.\"\n\nSide by side:\n\n| | Next.js 14 | Next.js 15\u002F16, default model | Next.js 16, `cacheComponents: true` |\n| --- | --- | --- | --- |\n| `GET` with no dynamic input | Evaluated at build time, cached | Runs on every request | Prerendered only if it touches no uncached or runtime data |\n| The scenario's DB query | Cached (the $40 bug) | Runs per request | Runs per request (a DB query stops prerendering) |\n| Cache it on purpose | Already cached; `revalidate` for a window | `export const dynamic = \"force-static\"` | A `\"use cache\"` helper with `cacheLife` |\n| Non-`GET` methods | Never cached | Never cached | Never cached |\n\nThe last row never changes: \"Other supported HTTP methods are **not** cached, even if they are placed alongside a `GET` method that is cached, in the same file.\"\n\nHold on to one inversion and the rest of this article follows: **in 14 you wrote config to get out of the cache; in 16 you write config, or `\"use cache\"`, to get in.**\n\n## Caching Next.js Route Handlers on purpose in 16\n\n### The file convention\n\nA Route Handler lives in a `route.ts` (or `.js`) file under `app\u002F` and exports one async function per HTTP method: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS`. Next.js reads the exported names; there's no router table. A method you didn't export gets `405 Method Not Allowed`, and if you don't export `OPTIONS`, Next.js implements it and sets the `Allow` header for you.\n\n```ts\n\u002F\u002F app\u002Fapi\u002Fproducts\u002Froute.ts\nexport async function GET(request: Request) {\n  return Response.json({ ok: true });\n}\n\nexport async function POST(request: Request) {\n  const body = await request.json();\n  return Response.json({ received: body }, { status: 201 });\n}\n```\n\n**Key concept:** a `route.ts` and a `page.tsx` can't share a route segment. `app\u002Fpage.js` plus `app\u002Froute.js` is a conflict; `app\u002Fpage.js` plus `app\u002Fapi\u002Froute.js` is fine.\n\n### Request and response\n\nPlain Web `Request`\u002F`Response` are enough for a JSON API. The `request` argument is actually a `NextRequest`, which adds `request.nextUrl` (a parsed URL) and `request.cookies`.\n\n```ts\nimport type { NextRequest } from \"next\u002Fserver\";\n\nexport async function GET(request: NextRequest) {\n  const query = request.nextUrl.searchParams.get(\"q\");\n  return Response.json({ query });\n}\n```\n\n**Key concept:** reading the request changes nothing under the default model, because the handler already runs per request. Under `cacheComponents` it matters: \"request object properties (like `req.url`, `request.headers`, `request.cookies`, `request.body`)\" are on the docs' list of things that stop a `GET` handler from prerendering.\n\n### Dynamic segments: `params` is a Promise\n\nFor `app\u002Fapi\u002Fproducts\u002F[id]\u002Froute.ts`, the handler's second argument carries `params`, and since Next.js 15 it's a Promise you `await`:\n\n```ts\n\u002F\u002F app\u002Fapi\u002Fproducts\u002F[id]\u002Froute.ts\nimport { db } from \"@\u002Flib\u002Fdb\";\n\nexport async function GET(\n  request: Request,\n  ctx: RouteContext\u003C\"\u002Fapi\u002Fproducts\u002F[id]\">\n) {\n  const { id } = await ctx.params;\n  const product = await db.get(id);\n  if (!product) {\n    return Response.json({ error: \"not found\" }, { status: 404 });\n  }\n  return Response.json(product);\n}\n```\n\n`RouteContext` is a global type helper generated by `next dev`, `next build` or `next typegen`; the hand-written `{ params }: { params: Promise\u003C{ id: string }> }` is equivalent.\n\nNext.js 15 made `params`, `cookies()` and `headers()` async with a grace period: \"these APIs can temporarily be accessed synchronously, but will show warnings in development and production until the next major version.\" That major was 16: \"synchronous access is fully removed.\" If your upgrade skipped the migration, run the codemod: `npx @next\u002Fcodemod@canary next-async-request-api .`\n\n### Default model: opt in with `force-static`\n\nThis replaces \"do nothing\" from 14. The docs: \"To cache a `GET` method, use a route config option such as `export const dynamic = 'force-static'` in your Route Handler file.\"\n\n```ts\n\u002F\u002F app\u002Fapi\u002Fproducts\u002Froute.ts\nimport { db } from \"@\u002Flib\u002Fdb\";\n\nexport const dynamic = \"force-static\"; \u002F\u002F evaluate once, cache the response\nexport const revalidate = 3600; \u002F\u002F optional: refresh at most once an hour (seconds)\n\nexport async function GET() {\n  const products = await db.query(\"SELECT id, name, price FROM products\");\n  return Response.json(products);\n}\n```\n\nWithout `revalidate`, you've recreated the Next.js 14 behaviour on purpose: one evaluation, reused until you redeploy or invalidate it. For on-demand refresh, call `revalidatePath(\"\u002Fapi\u002Fproducts\")` from the handler that writes; its reference lists Route Handler paths among what it can invalidate.\n\n**Key concept:** `force-static` works by \"forcing `cookies`, `headers()` and `useSearchParams()` to return empty values,\" so a `force-static` handler that reads a cookie silently sees none. Reserve it for responses that are the same for every caller.\n\n### Cache Components: `GET` handlers prerender like pages\n\nEnable the flag and the segment configs go away: they \"are replaced by `use cache` and `cacheLife`.\" A `GET` handler is now judged by what it touches, like a page (the model from [Next.js Cache Components Explained](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-cache-components-explained)).\n\n```ts\n\u002F\u002F Prerendered at build time: no uncached or runtime data.\nexport async function GET() {\n  return Response.json({ projectName: \"Next.js\" });\n}\n```\n\nThe scenario's handler, with its async database query, does *not* prerender. Prerendering stops on \"network requests, database queries, async file system operations, request object properties (…), runtime APIs like `cookies()`, `headers()`, `connection()`, or non-deterministic operations.\" To cache the query, move it into a helper marked `\"use cache\"`:\n\n```ts\n\u002F\u002F app\u002Fapi\u002Fproducts\u002Froute.ts\nimport { cacheLife, cacheTag } from \"next\u002Fcache\";\nimport { db } from \"@\u002Flib\u002Fdb\";\n\nasync function getProducts() {\n  \"use cache\";\n  cacheLife(\"hours\");\n  cacheTag(\"products\");\n  return db.query(\"SELECT id, name, price FROM products\");\n}\n\nexport async function GET() {\n  const products = await getProducts();\n  return Response.json(products);\n}\n```\n\nTwo rules from the docs: \"`use cache` cannot be used directly inside a Route Handler body; extract it to a helper function,\" and \"Cached responses revalidate according to `cacheLife` when a new request arrives.\" The `cacheTag` line lets a write elsewhere invalidate it.\n\n**Key concept:** this is \"cache the piece, not the route,\" the idea Cache Components brought to pages. In a Route Handler, the piece is a helper function instead of a component.\n\n### Streaming a response\n\nA Route Handler can stream its body instead of buffering it, which suits server-sent events, long exports, or proxying a slow upstream:\n\n```ts\n\u002F\u002F app\u002Fapi\u002Fticks\u002Froute.ts\nimport { connection } from \"next\u002Fserver\";\n\nexport async function GET() {\n  await connection(); \u002F\u002F under cacheComponents: explicitly request-time\n  const encoder = new TextEncoder();\n  const stream = new ReadableStream({\n    async start(controller) {\n      for (let i = 0; i \u003C 5; i++) {\n        controller.enqueue(encoder.encode(`event: tick\\ndata: ${i}\\n\\n`));\n        await new Promise((r) => setTimeout(r, 1000));\n      }\n      controller.close();\n    },\n  });\n\n  return new Response(stream, {\n    headers: { \"Content-Type\": \"text\u002Fevent-stream\" },\n  });\n}\n```\n\nUnder the default model the `connection()` line is optional; the handler already runs per request. Under `cacheComponents` it states the intent, since `connection()` is on the docs' list of calls that stop prerendering.\n\n**Key concept:** this is unrelated to a page's `\u003CSuspense>` streaming. Pages stream *rendered HTML*; a Route Handler streams *whatever bytes you enqueue*.\n\n## Edge cases and gotchas\n\n- **Non-`GET` methods are never cached, in any version or model.** A cached `GET` doesn't make the `POST` beside it cached, and there's no config that does.\n- **A cached `GET` doesn't know about writes from elsewhere.** Invalidate from the `POST` handler or webhook that writes. Under `cacheComponents`, call `revalidateTag(\"products\", \"max\")` (the single-argument form is deprecated in 16). Not `updateTag`: it \"can only be called from a Server Action; calling it elsewhere throws.\" Under the default model, use `revalidatePath(\"\u002Fapi\u002Fproducts\")`.\n- **A synchronous database driver can bring the 14-era bug back under `cacheComponents`.** The caching guide says queries to \"embedded databases with synchronous APIs, such as `better-sqlite3` or Node.js's built-in `node:sqlite`\" complete during prerendering, and `GET` handlers follow the same model. If the answer must be live, `await connection()` before the query.\n- **`try\u002Fcatch` catches the prerender bail-out.** Under `cacheComponents`, reading uncached or runtime data \"bails out of prerendering by **throwing**,\" so a `try\u002Fcatch` that logs adds noise to the build output (the docs point to `experimental.hideLogsAfterAbort: true`).\n- **Metadata routes kept the old default.** \"Special Route Handlers like `sitemap.ts`, `opengraph-image.tsx`, and `icon.tsx`, and other metadata files remain static by default unless they use Request-time APIs or dynamic config options.\"\n- **No layouts, no `error.tsx`.** Route Handlers aren't part of the React tree; handle failures with `try\u002Fcatch` and explicit status codes.\n- **CORS headers are manual.** The automatic `OPTIONS` response sets `Allow`, not `Access-Control-*`. Set them on your responses, or for many handlers at once in [`proxy.ts`](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-proxyts-explained-with-cheat-sheet) or `next.config` headers.\n- **`export const runtime = \"edge\"` is deprecated.** \"The Edge Runtime is deprecated. Remove the `runtime` export from your route files.\" Node.js is the default, and Cache Components requires it.\n\n## Best practices: Route Handler, Server Action, or page?\n\nReach for a **Route Handler** when the caller isn't your own App Router UI: a Stripe or GitHub webhook, an OAuth callback, a public API, or a response that isn't HTML, such as a file download, an RSS feed or an SSE stream.\n\nReach for a **Server Action**, covered in [Server Actions, Mutations & Security](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-server-actions-mutations-security-cheat-sheet), when a form or button in your own UI mutates data. It's also the only place `updateTag`'s read-your-own-writes refresh works.\n\nReach for a **page's own data fetching** when nothing outside your app needs the data. A Route Handler that exists only so your own page can `fetch()` it is usually one hop you don't need.\n\nWhichever you pick, **write the caching decision into the file.** A reviewer can see a `force-static` line or a `\"use cache\"` helper. A default is something they have to know, and this one has already changed once.\n\n## FAQ\n\n### Are Next.js Route Handlers cached by default?\n\nNo, not since Next.js 15. Under the default model, a `GET` handler runs on every request until you add `export const dynamic = \"force-static\"`; other methods are never cached. Under `cacheComponents`, a `GET` handler is prerendered only if it touches no uncached or runtime data.\n\n### Why did my GET Route Handler stop being cached after upgrading to Next.js 15?\n\nBecause 15 changed the default from static to dynamic; in 14 it was cached only because nothing opted it out. Add `export const dynamic = \"force-static\"` (plus `revalidate` for a refresh window), or a `\"use cache\"` helper under `cacheComponents`.\n\n### How do I cache a Route Handler when cacheComponents is enabled?\n\nMove the data access into a helper marked `\"use cache\"`, give it a `cacheLife` (and a `cacheTag` if writes should invalidate it), and call it from the handler. The directive can't go in the handler body, and `dynamic`, `revalidate` and `fetchCache` exports error under the flag.\n\n### Can I use cookies() or headers() inside a Route Handler?\n\nYes. Import them from `next\u002Fheaders` and `await` them; both are async since 15, and synchronous access is removed in 16. Under `force-static` they return empty values, and under `cacheComponents` calling them keeps the handler at request time.\n\n### Why does my dynamic segment's params need an await?\n\nNext.js 15 made the request-time APIs (`params`, `searchParams`, `cookies()`, `headers()`, `draftMode()`) asynchronous, so the framework knows when work has to wait for a request. 15 allowed synchronous access with warnings; 16 removed it.\n\n## Cheat sheet\n\n| Task | Code | Notes |\n| --- | --- | --- |\n| Define a handler | `export async function GET(req: Request) {}` | One export per method in `route.ts`; unexported methods get 405 |\n| Default for `GET` (15+, default model) | nothing to write | Runs on every request; was static in 14 |\n| Cache a `GET` (default model) | `export const dynamic = \"force-static\"` | The documented opt-in since 15 |\n| Add a refresh window (default model) | `export const revalidate = 3600` | Seconds; pair with `force-static` |\n| Refresh on demand (default model) | `revalidatePath(\"\u002Fapi\u002Fproducts\")` | Call from the handler that writes |\n| Cache under `cacheComponents` | `\"use cache\"` + `cacheLife(\"hours\")` in a helper | Not in the handler body; segment configs error |\n| Invalidate under `cacheComponents` | `cacheTag(\"products\")` → `revalidateTag(\"products\", \"max\")` | `updateTag` is Server Actions only |\n| Force request time (`cacheComponents`) | `await connection()` | From `next\u002Fserver` |\n| Read a dynamic segment | `const { id } = await ctx.params` | Promise since 15; sync access removed in 16 |\n| Type the context | `ctx: RouteContext\u003C\"\u002Fapi\u002Fproducts\u002F[id]\">` | Generated by `next dev` \u002F `build` \u002F `typegen` |\n| Read a query string | `request.nextUrl.searchParams.get(\"q\")` | Stops prerendering under `cacheComponents` |\n| Read a cookie | `(await cookies()).get(\"name\")` | From `next\u002Fheaders`; empty under `force-static` |\n| Stream a response | `new Response(new ReadableStream({ ... }))` | SSE, exports, proxying |\n| Runtime | delete `export const runtime = \"edge\"` | Edge is deprecated; Node.js is the default |\n\n## Key takeaways\n\n- Next.js 15 flipped `GET` Route Handlers from cached to dynamic by default. Next.js 14 code and tutorials describe the opposite default.\n- In 14 you wrote config to opt *out* of the cache; in 16 you write it to opt *in*: `force-static` under the default model, a `\"use cache\"` helper under `cacheComponents`.\n- Under `cacheComponents`, `GET` handlers follow the page rule: prerendered unless they touch uncached or runtime data, and `dynamic`\u002F`revalidate`\u002F`fetchCache` exports error.\n- `params` is a Promise; 15 tolerated synchronous access, 16 removed it.\n- Non-`GET` methods are never cached. Choose a Route Handler for callers outside your own UI and a Server Action for your own forms.\n\nThe $40 answer and the query-on-every-request are the same missing decision, seen from two versions. In 14 the framework decided \"cache it\"; in 15 it decided \"don't\". Either way the handler had a caching policy that nobody wrote down. Put the `force-static`, `revalidate` or `\"use cache\"` line in the file yourself, and the next change of default can't surprise you.\n\nWhich Route Handler in your app is still relying on a default it inherited from 14? Tell me in the comments.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-route-handlers-caching-streaming\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 8-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-route-handlers-caching-streaming\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n\n\u003C!-- related:start -->\n\n## 📚 Read next\n\n- [You Don't Need a WebSocket for That Live Feed](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fserver-sent-events-eventsource-live-updates)\n- [Your Fetch Already Streams. You're Buffering It Anyway.](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Ffetch-already-streams-readablestream)\n- [Next.js proxy.ts Explained (with Cheat Sheet)](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-middleware-to-proxy-network-boundary)\n\n\u003C!-- related:end -->\n\n---\n\n🚀 **Want more like this?** Every guide, playground, and quiz lives on **[bestpractic.org](https:\u002F\u002Fbestpractic.org\u002F)** — open it and **[sign up free](https:\u002F\u002Fbestpractic.org\u002F)** so the next one finds you.\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)",{"title":96,"canonical":558,"description":98},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-route-handlers-caching-streaming","01a0ec73-0927-7298-a1ae-8e795c9058df",{"name":561,"part":562,"total":562,"items":563},"Next.js Deep Dive",5,[564,568,572,576,581],{"slug":565,"title":566,"publishedAt":567,"readingMinutes":72},"nextjs-weekly-cache-components-explained","Next.js Cache Components Explained (with Cheat Sheet)","2026-09-01T11:24:15.087Z",{"slug":569,"title":570,"publishedAt":571,"readingMinutes":100},"nextjs-weekly-server-actions-mutations-security","Next.js Server Actions: Mutations & Security (Cheat Sheet)","2026-09-08T10:57:00.975Z",{"slug":573,"title":574,"publishedAt":575,"readingMinutes":72},"nextjs-weekly-parallel-intercepting-routes-modals","Next.js Parallel & Intercepting Routes: Modals Done Right","2026-09-15T17:50:47.218Z",{"slug":577,"title":578,"publishedAt":579,"readingMinutes":580},"nextjs-weekly-middleware-to-proxy-network-boundary","Next.js proxy.ts Explained (with Cheat Sheet)","2026-09-22T19:36:08.416Z",12,{"slug":95,"title":96,"publishedAt":101,"readingMinutes":100},{"id":583,"locked":18},"01a0ec73-0961-70cc-9dd5-b987b7c212d8",[585],{"id":586,"slug":95,"title":587,"_count":588},"01a0ec73-097a-73a9-8a1d-fe9a0510cac1","Next.js Route Handlers: GET Caching Since 15, and How to Cache in 16",{"questions":88},[590],{"locale":13,"slug":95},{"id":586,"slug":95,"title":587,"_count":592,"questionCount":88},{"questions":88},{"items":594,"meta":667},[595,607,622,637,652],{"id":94,"slug":95,"title":96,"subtitle":97,"excerpt":98,"coverUrl":99,"locale":13,"readingMinutes":100,"publishedAt":101,"viewCount":596,"likeCount":19,"commentCount":19,"author":597,"vertical":598,"topic":599,"tags":600,"_count":605,"playground":606,"hasQuiz":17,"hasPlayground":17},62,{"id":104,"name":105,"username":106,"avatarUrl":97,"headline":107},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":110,"name":111},[601,602,603,604],{"slug":110,"name":111,"color":97},{"slug":66,"name":67,"color":97},{"slug":46,"name":47,"color":97},{"slug":58,"name":59,"color":97},{"assessments":118},{"slug":95},{"id":608,"slug":577,"title":578,"subtitle":97,"excerpt":609,"coverUrl":610,"locale":13,"readingMinutes":580,"publishedAt":579,"viewCount":611,"likeCount":19,"commentCount":19,"author":612,"vertical":613,"topic":614,"tags":615,"_count":620,"playground":621,"hasQuiz":17,"hasPlayground":17},"01a0b58a-7f56-736a-babf-1796c7b70c19","Next.js 16 renamed middleware.ts to proxy.ts and locked it to the Node.js runtime. Learn the network-boundary model, matcher config, and the migration path.","\u002Fmedia\u002Fcovers\u002Fnextjs-weekly-middleware-to-proxy-network-boundary.png",256,{"id":104,"name":105,"username":106,"avatarUrl":97,"headline":107},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":110,"name":111},[616,617,618,619],{"slug":110,"name":111,"color":97},{"slug":50,"name":51,"color":97},{"slug":58,"name":59,"color":97},{"slug":46,"name":47,"color":97},{"assessments":118},{"slug":577},{"id":623,"slug":573,"title":574,"subtitle":97,"excerpt":624,"coverUrl":625,"locale":13,"readingMinutes":72,"publishedAt":575,"viewCount":626,"likeCount":19,"commentCount":19,"author":627,"vertical":628,"topic":629,"tags":630,"_count":635,"playground":636,"hasQuiz":17,"hasPlayground":17},"01a09181-2f3b-7349-b651-fd9cdda0b931","How Next.js parallel routes (@slot) and intercepting routes ((.), (..), (...)) combine to build shareable, refreshable modals — verified against Next.js 16.3.","\u002Fmedia\u002Fcovers\u002Fnextjs-weekly-parallel-intercepting-routes-modals.png",321,{"id":104,"name":105,"username":106,"avatarUrl":97,"headline":107},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":110,"name":111},[631,632,633,634],{"slug":110,"name":111,"color":97},{"slug":74,"name":75,"color":97},{"slug":46,"name":47,"color":97},{"slug":58,"name":59,"color":97},{"assessments":118},{"slug":573},{"id":638,"slug":569,"title":570,"subtitle":97,"excerpt":639,"coverUrl":640,"locale":13,"readingMinutes":100,"publishedAt":571,"viewCount":641,"likeCount":19,"commentCount":19,"author":642,"vertical":643,"topic":644,"tags":645,"_count":650,"playground":651,"hasQuiz":17,"hasPlayground":17},"01a06d6c-41f6-77bd-95e3-cdd69a0f72eb","Next.js Server Actions look like plain functions but compile to public POST endpoints. Learn the mutation flow, built-in CSRF checks, and the auth you owe.","\u002Fmedia\u002Fcovers\u002Fnextjs-weekly-server-actions-mutations-security.png",313,{"id":104,"name":105,"username":106,"avatarUrl":97,"headline":107},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":110,"name":111},[646,647,648,649],{"slug":110,"name":111,"color":97},{"slug":74,"name":75,"color":97},{"slug":46,"name":47,"color":97},{"slug":58,"name":59,"color":97},{"assessments":118},{"slug":569},{"id":653,"slug":565,"title":566,"subtitle":97,"excerpt":654,"coverUrl":655,"locale":13,"readingMinutes":72,"publishedAt":567,"viewCount":656,"likeCount":19,"commentCount":19,"author":657,"vertical":658,"topic":659,"tags":660,"_count":665,"playground":666,"hasQuiz":17,"hasPlayground":17},"01a0499d-b520-778f-8286-8221ec03b8b5","How Next.js Cache Components decide what's static, what's cached, and what streams — the use cache directive, cacheLife, and Suspense explained.","\u002Fmedia\u002Fcovers\u002Fnextjs-weekly-cache-components-explained.png",435,{"id":104,"name":105,"username":106,"avatarUrl":97,"headline":107},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":110,"name":111},[661,662,663,664],{"slug":110,"name":111,"color":97},{"slug":74,"name":75,"color":97},{"slug":46,"name":47,"color":97},{"slug":58,"name":59,"color":97},{"assessments":118},{"slug":565},{"page":118,"perPage":92,"total":562,"totalPages":118},"\u003Cdiv class=\"shj shj-lang-ts shj-multiline\" data-lang=\"ts\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F app\u002Fapi\u002Fproducts\u002Froute.ts\n\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> db \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"@\u002Flib\u002Fdb\"\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-num\">GET\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> products \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> db\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">query\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"SELECT id, name, price FROM products\"\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Response\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">json\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>products\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-ts shj-multiline\" data-lang=\"ts\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F app\u002Fapi\u002Fproducts\u002Froute.ts\n\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-num\">GET\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>request\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Request\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Response\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">json\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> ok\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bool\">true\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\n\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-num\">POST\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>request\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Request\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> body \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> request\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">json\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Response\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">json\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> received\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> body \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> status\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-num\">201\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-ts shj-multiline\" data-lang=\"ts\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">type\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> \u003Cspan class=\"shj-class\">NextRequest\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"next\u002Fserver\"\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-num\">GET\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>request\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-class\">NextRequest\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> query \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> request\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>nextUrl\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>searchParams\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">get\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"q\"\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Response\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">json\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> query \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-ts shj-multiline\" data-lang=\"ts\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003Cdiv>13\u003C\u002Fdiv>\u003Cdiv>14\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F app\u002Fapi\u002Fproducts\u002F[id]\u002Froute.ts\n\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> db \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"@\u002Flib\u002Fdb\"\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-num\">GET\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\n  request\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Request\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n  ctx\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-class\">RouteContext\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"\u002Fapi\u002Fproducts\u002F[id]\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> id \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> ctx\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>params;\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> product \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> db\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">get\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>id\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">if\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">!\u003C\u002Fspan>product\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Response\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">json\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> error\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"not found\"\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> status\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-num\">404\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Response\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">json\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>product\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-ts shj-multiline\" data-lang=\"ts\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F app\u002Fapi\u002Fproducts\u002Froute.ts\n\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> db \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"@\u002Flib\u002Fdb\"\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> dynamic \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"force-static\"\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F evaluate once, cache the response\n\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> revalidate \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-num\">3600\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F optional: refresh at most once an hour (seconds)\n\u003C\u002Fspan>\n\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-num\">GET\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> products \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> db\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">query\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"SELECT id, name, price FROM products\"\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Response\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">json\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>products\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-ts shj-multiline\" data-lang=\"ts\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F Prerendered at build time: no uncached or runtime data.\n\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-num\">GET\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Response\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">json\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> projectName\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"Next.js\"\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-ts shj-multiline\" data-lang=\"ts\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003Cdiv>13\u003C\u002Fdiv>\u003Cdiv>14\u003C\u002Fdiv>\u003Cdiv>15\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F app\u002Fapi\u002Fproducts\u002Froute.ts\n\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> cacheLife\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> cacheTag \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"next\u002Fcache\"\u003C\u002Fspan>;\n\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> db \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"@\u002Flib\u002Fdb\"\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">getProducts\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-str\">\"use cache\"\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-func\">cacheLife\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"hours\"\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-func\">cacheTag\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"products\"\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> db\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">query\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"SELECT id, name, price FROM products\"\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\n\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-num\">GET\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> products \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> \u003Cspan class=\"shj-func\">getProducts\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Response\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">json\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>products\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-ts shj-multiline\" data-lang=\"ts\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003Cdiv>13\u003C\u002Fdiv>\u003Cdiv>14\u003C\u002Fdiv>\u003Cdiv>15\u003C\u002Fdiv>\u003Cdiv>16\u003C\u002Fdiv>\u003Cdiv>17\u003C\u002Fdiv>\u003Cdiv>18\u003C\u002Fdiv>\u003Cdiv>19\u003C\u002Fdiv>\u003Cdiv>20\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F app\u002Fapi\u002Fticks\u002Froute.ts\n\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> connection \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"next\u002Fserver\"\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-num\">GET\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> \u003Cspan class=\"shj-func\">connection\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F under cacheComponents: explicitly request-time\n\u003C\u002Fspan>  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> encoder \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">TextEncoder\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> stream \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">ReadableStream\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    \u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-func\">start\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>controller\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n      \u003Cspan class=\"shj-kwd\">for\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">let\u003C\u002Fspan> i \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-num\">0\u003C\u002Fspan>; i \u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan> \u003Cspan class=\"shj-num\">5\u003C\u002Fspan>; i\u003Cspan class=\"shj-oper\">++\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n        controller\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">enqueue\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>encoder\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">encode\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">`event: tick\\ndata: \u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">${\u003C\u002Fspan>i\u003Cspan class=\"shj-kwd\">}\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\\n\\n`\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n        \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Promise\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>r\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-func\">setTimeout\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>r\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-num\">1000\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n      \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n      controller\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">close\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n    \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Response\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>stream\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    headers\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> \"Content-Type\"\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"text\u002Fevent-stream\"\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>",{"locked":18,"total":19,"comments":677},[]]