cf.completefrontend
Open lab My progress
THE NEXT.JS FIELD GUIDE

Upgrading Next.js

Move a Next.js 14 app to 16: read release notes, run real codemods, fix async request APIs, rename middleware to proxy, and upgrade safely.

By the end, you can
  • 01
    Read what changedFind a version's release post, upgrade guide and deprecation warnings, and tell a breaking change from a new option.
  • 02
    Let codemods do the routine workRun @next/codemod on a clean branch, read its diff, and fix by hand what it marks with @next-codemod-error.
  • 03
    Move across majors safelyHandle async request APIs, proxy.ts, Turbopack, removed options and Cache Components, and check each step with a build and tests.

A tea shop written for Next.js 14

The tea shop was built in 2024, with Next.js 14, and nobody has touched its framework since. Upgrading means moving it to a newer version of Next.js, and changing the code the new version treats differently. The first step looks easy: install 16.4.3 and the React it comes with, then build. Here is that build, a real one. Open the Terminal tab.

The Next.js 14 tea shop, built with Next.js 16.4.3Next.js
app/teas/[id]/
app/teas/[id]/page.tsx
import { notFound } from "next/navigation";import { getCurrency } from "../../currency";import { teas } from "../../teas"; export function generateMetadata({ params }: { params: { id: string } }) {  const tea = teas.find((item) => item.id === params.id);  return { title: tea ? tea.name : "Tea shop" };} export default function TeaPage({ params }: { params: { id: string } }) {  const tea = teas.find((item) => item.id === params.id);  if (!tea) notFound();  return (    <main>      <h1>{tea.name}</h1>      <p>        {tea.price} {getCurrency()}      </p>    </main>  );}
next build failed · Next.js 16.4.3
$ next build▲ Next.js 16.4.3 (Turbopack) ⚠ The "middleware" file convention is deprecated. Please use "proxy" instead.   To migrate automatically, run:  npx @next/codemod@canary middleware-to-proxy .   Learn more: https://nextjs.org/docs/messages/middleware-to-proxy  Creating an optimized production build ...✓ Compiled successfully  Running TypeScript ...app/currency.ts(4,20): error TS2339: Property 'get' does not exist on type 'Promise<ReadonlyRequestCookies>'.Failed to type check.

Two things in the output. First a warning: The "middleware" file convention is deprecated. Please use "proxy" instead. Deprecated means it still works, but it's planned to be removed, and the warning names the command that changes it. Then TypeScript stops the build at line 4 of app/currency.ts: Property 'get' does not exist on type 'Promise<ReadonlyRequestCookies>'. In Next.js 14, cookies() returned the cookies. Since 15 it returns a promise of them. The tea's page reads params the old way too, on lines 5 and 10 of app/teas/[id]/page.tsx.

Four tags on a line, joined by arrows. 14, October 2023: Server Actions; no longer supported. 15, October 2024: async params; maintenance. 16, October 2025: proxy.ts and Turbopack; active. 16.4, October 2026: upgrade --agent; the latest minor. The tea shop starts at 14 and moves to 16.4.14Oct 2023Server Actionsunsupported15Oct 2024async paramsmaintenance16Oct 2025proxy.ts, Turbopackactive16.4Oct 2026upgrade --agentlatest minorthe shop starts hereand ends here
The tea shop was written for Next.js 14 and moves to 16.4, crossing two majors. Each major has its own upgrade guide, and the docs ask you to follow the guide for every major you cross.
Real-life analogyA city that renames its streets

When a city renames streets, it publishes the list first. Old signs stay up for a while, with the new name below. The post office runs every typed letter through a machine that fixes the address. The letters it can't read go to a clerk, who works them out by hand.

In real life: The city's list of renamed streets
In Next.js: Release notes and the upgrade guide
In real life: Old signs stay up for a while, under the new names
In Next.js: Deprecated APIs keep working, with a warning
In real life: A machine reprints the typed addresses on letters
In Next.js: A codemod rewrites the code it understands
In real life: Handwritten letters go to a clerk
In Next.js: @next-codemod-error: code you fix by hand
The main idea

Upgrading is reading what changed, letting codemods make the routine changes, fixing the rest by hand, and proving the result with a build and tests. Next.js says what changes in release posts, upgrade guides and warnings, and @next/codemod rewrites the code it can. Do it on a branch, one step at a time.

Where changes are announced

A Next.js version has three numbers, like 16.4.3: the major (16), the minor (4) and the patch (3). This is called semantic versioning. A patch fixes bugs. A minor adds features, and can mark old ones as deprecated. A major can remove and change APIs, so code that worked can break. Next.js follows this with one exception, written in its support policy: fixes for a major in Maintenance LTS come out as minor releases, even when they break something. The policy also says what each major gets:

  • Next.js 14, released October 26, 2023: No longer supported.
  • Next.js 15, released October 21, 2024: Maintenance LTS, with security fixes until January 31, 2027.
  • Next.js 16, released October 21, 2025: Active LTS.

Active LTS gets new features and fixes. Maintenance LTS gets only critical and security fixes, normally for two years after the major's release; for 15 the policy extended it. The tea shop's 14 gets nothing, not even security fixes. Each row is one place where changes are announced:

Where to read what changed
WhereWhat it tells youFrom the tea shop's upgrade
nextjs.org/blogA post for each major and minor release, and the security posts: notice of a security release about once a month, then the release.The Next.js 16.4 post, October 6, 2026.
The upgrade guidesOne for each major: what breaks, the codemod for each change, and a checklist at the end.How to upgrade to version 16, in the docs' Upgrading section.
GitHub releasesEvery published version, canaries too, with the changes merged into it.The v16.4.3 release and its list of changes.
The support policyWhich majors still get fixes, and for how long. Fixes for a major in Maintenance LTS come out as minor releases, even when they break something.15 gets security fixes until January 31, 2027.
Your terminalDeprecation warnings from next build and next dev, often with the codemod to run.The "middleware" file convention is deprecated.
node_modules/next/dist/docs/The docs for the version you have installed, which coding agents read.16.4.3's own copy of the version 16 guide.

npm gives some versions a name, a dist-tag, and npm view next dist-tags lists them. Each row is one tag, as it was on October 30, 2026:

Next.js's dist-tags on npm
TagWhat it meansOn that day
latestThe newest stable release. npm install next gets it.16.4.3
canaryPublished daily, with the newest changes. The support policy says not to serve production traffic from it.A 16.5.0-canary version
rc, beta, previewVersions before a release, for trying it early: a release candidate is meant to become the release unless a problem shows up.Older ones, like 16.0.0-beta.0
backportFixes for the previous major, which is still in Maintenance LTS.A 15.5 version

Security fixes have a schedule of their own. Since July 2026, the Next.js blog announces a security release about once a month, with its date and how severe the worst problem is, so you can plan the upgrade; urgent ones still come at once. Read a security post like you read a release post: which versions are affected, and which patch fixes it. (The middleware bypass walks through one.)

Codemods: changes made by a program

A codemod is a program that rewrites source code. @next/codemod reads each file into a tree, the way a compiler does, changes the parts one migration is about, and writes the file back. You run it in the project's folder with the transform's name and a path: npx @next/codemod@16.4.3 next-async-request-api .. It refuses to run while git has uncommitted changes (--force skips that), so that its changes are the only ones in git diff. The recording did the same: it committed the shop's files, ran two codemods, and showed the diff after each one. Open the Terminal tab:

Two codemods on the tea shop, then a buildNext.js
app/teas/[id]/
app/teas/[id]/page.tsx
import { notFound } from "next/navigation";import { getCurrency } from "../../currency";import { teas } from "../../teas"; export function generateMetadata({ params }: { params: { id: string } }) {  const tea = teas.find((item) => item.id === params.id);  return { title: tea ? tea.name : "Tea shop" };} export default function TeaPage({ params }: { params: { id: string } }) {  const tea = teas.find((item) => item.id === params.id);  if (!tea) notFound();  return (    <main>      <h1>{tea.name}</h1>      <p>        {tea.price} {getCurrency()}      </p>    </main>  );}
next build failed · Next.js 16.4.3
$ npx @next/codemod@16.4.3 next-async-request-api .Executing command: jscodeshift --parser=tsx --ignore-pattern=**/node_modules/** --ignore-pattern=**/.next/** --extensions=tsx,ts,jsx,js,mjs --transform /my-app/node_modules/@next/codemod/transforms/next-async-request-api.js .Processing 7 files...All done.Results:0 errors5 unmodified0 skipped2 ok$ git add -A && git diff --stageddiff --git a/app/currency.ts b/app/currency.tsindex a3bc85d..46ca70c 100644--- a/app/currency.ts+++ b/app/currency.ts@@ -1,5 +1,6 @@-import { cookies } from "next/headers";+import { cookies, type UnsafeUnwrappedCookies } from "next/headers";  export function getCurrency() {-  return cookies().get("currency")?.value ?? "USD";+  return (/* @next-codemod-error Await this API and update its callers; remove the temporary UnsafeUnwrappedCookies cast after repairing the migration. */+  cookies() as unknown as UnsafeUnwrappedCookies).get("currency")?.value ?? "USD"; }diff --git a/app/teas/[id]/page.tsx b/app/teas/[id]/page.tsxindex e2f6047..78c4836 100644--- a/app/teas/[id]/page.tsx+++ b/app/teas/[id]/page.tsx@@ -2,12 +2,14 @@ import { notFound } from "next/navigation"; import { getCurrency } from "../../currency"; import { teas } from "../../teas"; -export function generateMetadata({ params }: { params: { id: string } }) {+export async function generateMetadata(props: { params: Promise<{ id: string }> }) {+  const params = await props.params;   const tea = teas.find((item) => item.id === params.id);   return { title: tea ? tea.name : "Tea shop" }; } -export default function TeaPage({ params }: { params: { id: string } }) {+export default async function TeaPage(props: { params: Promise<{ id: string }> }) {+  const params = await props.params;   const tea = teas.find((item) => item.id === params.id);   if (!tea) notFound();   return ( $ npx @next/codemod@16.4.3 middleware-to-proxy .Executing command: jscodeshift --parser=tsx --ignore-pattern=**/node_modules/** --ignore-pattern=**/.next/** --extensions=tsx,ts,jsx,js,mjs --transform /my-app/node_modules/@next/codemod/transforms/middleware-to-proxy.js .Processing 7 files...All done.Results:0 errors6 unmodified1 skipped0 ok$ git add -A && git diff --stageddiff --git a/middleware.ts b/proxy.tssimilarity index 85%rename from middleware.tsrename to proxy.tsindex f5dfc92..e25a32e 100644--- a/middleware.ts+++ b/proxy.ts@@ -1,7 +1,7 @@ import { NextResponse, type NextRequest } from "next/server";  // Old links said /tea/mint. Send them to /teas/mint.-export function middleware(request: NextRequest) {+export function proxy(request: NextRequest) {   const id = request.nextUrl.pathname.slice("/tea/".length);   return NextResponse.redirect(new URL(`/teas/${id}`, request.url)); } $ next build▲ Next.js 16.4.3 (Turbopack)   Creating an optimized production build ... > Build error occurredError: Turbopack build failed with 1 error:./app/currency.ts:4:10Error: You have an unresolved @next/codemod comment "Await this API and update its callers; remove the temporary UnsafeUnwrappedCookies cast after repairing the migration." that needs review.    After review, either remove the comment if you made the necessary changes or replace "@next-codemod-error" with "@next-codemod-ignore" to bypass the build error if no action at this line can be taken.  2 | ...  3 | ...> 4 | ...t this API and update its callers; remove the temporary UnsafeUnwrappedCookies cast afte...    |    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^  5 | ...ppedCookies).get("currency")?.value ?? "USD";  6 | ...  7 | ... Ecmascript file had an error Import trace:  Server Component:    ./app/currency.ts    ./app/teas/[id]/page.tsx     at ignore-listed frames

Read it from the top:

  • next-async-request-api looked at 7 files and changed 2. In app/teas/[id]/page.tsx, both functions became async, params became props.params typed as a Promise, and a new first line waits for it: const params = await props.params;.
  • In app/currency.ts, it didn't fix the code. It added a cast, as unknown as UnsafeUnwrappedCookies, so the file still type-checks, and a comment starting with @next-codemod-error: Await this API and update its callers.
  • middleware-to-proxy renamed middleware.ts to proxy.ts and its function to proxy. Git shows it as a rename, with 85% of the file the same.
  • Then the build stopped: You have an unresolved @next/codemod comment. Next.js refuses to build with one, so a cast that only hides the problem can't reach production.
Two rows. Top: page.tsx with sync params goes into the codemod, which writes page.tsx with await props.params. Bottom: currency.ts goes into the codemod, which writes a cast and an @next-codemod-error comment; the build refuses it until you fix it by hand.page.tsxparams.idcodemodpage.tsxawait props.paramscurrency.tscookies().getcodemod@next-codemod-errorbuild refuses ityou fix it by hand
next-async-request-api rewrote the tea's page, which it knows how to change. In app/currency.ts it couldn't tell how to change the callers, so it left a comment, and next build refuses to build until you deal with it.

Why couldn't it fix getCurrency? A page or layout can become async safely: Next.js is the only one that calls it. A helper has callers of its own, and making it async changes what every one of them gets. So you do it:

app/currency.ts, fixed by handTSX
import { cookies } from "next/headers"; export async function getCurrency() {  return (await cookies()).get("currency")?.value ?? "USD";} 

Line 3 makes getCurrency async, and line 4 awaits cookies() before it reads the cookie. The one caller, line 19 of the tea's page, becomes {await getCurrency()}. Now the shop builds. Open the Network tab: /teas/mint with the cookie currency=EUR says 3 EUR, and the old link /tea/mint answers 307 with location: /teas/mint, from proxy.ts.

The tea shop after the codemods and one fixNext.js
app/teas/[id]/
app/currency.ts
import { cookies } from "next/headers"; export async function getCurrency() {  return (await cookies()).get("currency")?.value ?? "USD";}
Recorded from Next.js 16.4.3

GET /teas/mint200

Sent with the request
cookie currency=EUR
Response headers
Vary rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch, Accept-Encoding
X-Powered-By Next.js
Cache-Control private, no-cache, no-store, max-age=0, must-revalidate
Content-Type text/html; charset=utf-8

The HTML that arrived, one element per line, with the code of its 10 scripts left out

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="preload" as="script" fetchpriority="low" href="/_next/static/chunks/1x6obpmlu1_c2.js">
    <script src="/_next/static/chunks/2r4s4h5xul1n-.js" async=""></script>
    <script src="/_next/static/chunks/3wbm-994r45kr.js" async=""></script>
    <script src="/_next/static/chunks/2e268anpfvdoc.js" async=""></script>
    <script src="/_next/static/chunks/turbopack-3-ry682tkyiqk.js" async=""></script>
    <script src="/_next/static/chunks/27gjg8e11zukd.js" async=""></script>
    <title>Mint tea</title>
    <script src="/_next/static/chunks/0cz1d0mv5g_q7.js" nomodule=""></script>
  </head>
  <body>
    <div hidden="">
      <!--$-->
      <!--/$-->
    </div>
    <nav><a href="/">Teas</a></nav>
    <main>
      <h1>Mint tea</h1>
      <p>3<!-- --> <!-- -->EUR</p>
    </main>
    <!--$-->
    <!--/$-->
    <script id="_R_">…</script>
    <script src="/_next/static/chunks/1x6obpmlu1_c2.js" async=""></script>
    <script>…</script>
    <script>…</script>
  </body>
</html>

15: request APIs that return promises

Next.js 15 changed the APIs that read the request. Each one now returns a promise:

  • cookies(), headers() and draftMode(), from next/headers.
  • The params prop of pages, layouts, Route Handlers, default.js files and metadata functions like generateMetadata and opengraph-image.
  • The searchParams prop, which only pages get, and their generateMetadata.

The reason: a page can start rendering before the request's data is needed, and only the parts that await it wait. Request-time APIs shows what that lets Next.js prerender.

Next.js 15 still let old code read them directly for a while, and logged a warning in development; its upgrade guide called that temporary. Next.js 16 removed it. A TypeScript project fails to build, like the old shop. A JavaScript project has no types to stop it, so the build passes, and the page fails when it runs. Here is the old page as a .jsx file. Open the Terminal tab, and look under next start:

The old page in JavaScript: it builds, then failsNext.js
app/teas/[id]/
app/teas/[id]/page.jsx
import { cookies } from "next/headers";import { teas } from "../../teas"; export default function TeaPage({ params }) {  const tea = teas.find((item) => item.id === params.id);  const currency = cookies().get("currency")?.value ?? "USD";  return (    <main>      <h1>{tea.name}</h1>      <p>        {tea.price} {currency}      </p>    </main>  );}
Recorded from Next.js 16.4.3
$ next build▲ Next.js 16.4.3 (Turbopack)   Creating an optimized production build ...✓ Compiled successfully  Running TypeScript ...  Finished TypeScript ...  Collecting page data ...✓ Generating static pages (2/2)  Finalizing page optimization ... Route (app)┌ ○ /_not-found└ ƒ /teas/[id] ○  (Static)   prerendered as static contentƒ  (Dynamic)  server-rendered on demand $ next start▲ Next.js 16.4.3- Local:         http://127.0.0.1:3000- Network:       http://127.0.0.1:3000✓ Ready⨯ TypeError: (0 , c.cookies)(...).get is not a function    at <unknown> (.next/server/chunks/ssr/_095i2nozvefnq._.js:1:223) {  digest: '3149127413'}

The build listed ƒ /teas/[id], with no error. The visit got a 500, and the server printed TypeError: … cookies(...).get is not a function, the call on line 6. Only something that asks for the page catches this: a test, or a preview deploy.

Server Components await these promises. A Client Component can't be async, so it unwraps one with React's use. This is from the version 15 guide, not recorded:

A Client Component page reads its params with useTSX
"use client";

import { use } from "react";

export default function TeaPage(props: { params: Promise<{ id: string }> }) {
  const { id } = use(props.params);
  return <h1>Tea: {id}</h1>;
}

Line 1 makes it a Client Component. Line 6 waits for props.params with use. The codemod writes the type by hand, { params: Promise<{ id: string }> }. Since 15.5, next typegen writes a type that knows each route's params, PageProps<"/teas/[id]">, and in 16.4.3 next build writes it too (Dynamic routes uses it). 15 also stopped caching fetch and GET Route Handlers by default: Caching before Cache Components explains that model.

16: proxy.ts, Turbopack and removed options

proxy.ts. Next.js 16 renamed middleware.ts to proxy.ts, and its function to proxy, to say what it is: code that runs in front of every matching request. The old name still works, with the warning the old shop printed. proxy.ts runs on Node.js only; the guide says an app that needs the Edge runtime keeps middleware.ts for now. Options with "middleware" in their names were renamed too, like skipMiddlewareUrlNormalize to skipProxyUrlNormalize, and the codemod changes them. The upgraded shop's route table lists it as ƒ Proxy (Middleware) (proxy.ts teaches it).

Turbopack by default. Every recorded build of the tea shop starts with ▲ Next.js 16.4.3 (Turbopack), without a flag: since 16, next dev and next build use Turbopack. Scripts like next build --turbopack can drop the flag, and --webpack keeps the old bundler. A custom webpack function in next.config is the catch: Turbopack doesn't run it.

Removed options. Some options from older versions are gone in 16. Try three versions of the upgraded shop's next.config.mjs:

Playground: what does next build say about this next.config.mjs?
The upgraded tea shop, with this version of next.config.mjsNext.js
app/teas/[id]/
next.config.mjs
/** @type {import('next').NextConfig} */const nextConfig = {  eslint: { ignoreDuringBuilds: true },  serverRuntimeConfig: { supplierKey: process.env.SUPPLIER_KEY },  experimental: { dynamicIO: true },}; export default nextConfig;
next build failed · Next.js 16.4.3
$ next build▲ Next.js 16.4.3 (Turbopack)⚠ `eslint` configuration in next.config.mjs is no longer supported. See more info here: https://nextjs.org/docs/app/api-reference/cli/next#next-lint-options⚠ Invalid next.config.mjs options detected:⚠     Unrecognized key(s) in object: 'eslint', 'serverRuntimeConfig'⚠ See more info here: https://nextjs.org/docs/messages/invalid-next-config > Build error occurredError: Fatal next config errors found in next.config.mjs that must be fixed:    Unrecognized key(s) in object: 'dynamicIO' at "experimental"    `experimental.dynamicIO` has been replaced by `cacheComponents`. Please update your next.config file accordingly.    Learn more: https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponentsThese configuration options are required or have been migrated. Please update your configuration.See more info here: https://nextjs.org/docs/messages/invalid-next-config    at ignore-listed frames
Try it yourself
As the shop had it

Three options from older versions. next build warns that eslint is no longer supported and that serverRuntimeConfig is unrecognized, then stops at line 5: experimental.dynamicIO has been replaced by cacheComponents.

Every version was built with next build from Next.js 16.4.3; the Terminal shows what it printed. Only next.config.mjs changes. Reset goes back to the first version.

The first version shows two kinds of message. Warnings, marked ⚠, for options Next.js no longer supports: eslint, because next build no longer lints, and serverRuntimeConfig, which 16 removed. And an error for one it can't ignore: `experimental.dynamicIO` has been replaced by `cacheComponents`. Here is that file:

next.config.mjs, with options from older versionsJavaScript
/** @type {import('next').NextConfig} */const nextConfig = {  eslint: { ignoreDuringBuilds: true },  serverRuntimeConfig: { supplierKey: process.env.SUPPLIER_KEY },  experimental: { dynamicIO: true },}; export default nextConfig; 

Line 3 is the eslint option, line 4 the runtime config, and line 5 the experiment that became Cache Components. Each row is one change from the version 16 guide: what happens to an app that still has it, and the fix:

What Next.js 16 removed or changed
What changedWhat happens to an app that still uses itThe fix
Sync params, searchParams, cookies(), headers(), draftMode()TypeScript stops the build. In JavaScript, the page fails when it runs.next-async-request-api, then fix what it marks.
middleware.tsStill works, with a deprecation warning. It's the only way to keep the Edge runtime.middleware-to-proxy. proxy.ts runs on Node.js only.
A webpack function in next.confignext build stops: Turbopack is the default and doesn't run it.Move it to turbopack options, or build with --webpack.
experimental.dynamicIOnext build stops: it has been replaced by cacheComponents.Remove it, or adopt Cache Components on purpose.
serverRuntimeConfig, publicRuntimeConfigRemoved. next build warns that the keys are unrecognized.Environment variables, with NEXT_PUBLIC_ only for public values.
next lint and the eslint optionRemoved; next build no longer lints.next-lint-to-eslint-cli, then eslint . in your scripts.
AMP, devIndicators.buildActivity, unstable_rootParamsRemoved.No AMP; nothing for the indicator; next/root-params.
Node.js 18, TypeScript below 5.1No longer supported.Node.js 20.9 or later, TypeScript 5.1 or later.

The guide lists more that the tea shop didn't meet: revalidateTag now takes a cache profile as its second argument (Revalidating data), next/image has new defaults, like a four-hour minimumCacheTTL and only quality 75 (Images), and every parallel route slot needs a default.js. The browsers it supports are Chrome, Edge and Firefox 111 and later, and Safari 16.4 and later.

Turning on Cache Components after the upgrade

The upgraded shop builds without Cache Components: its output has no Cache Components enabled line, and /teas/[id] is ƒ, made for every request. An upgrade doesn't turn them on, and the version 16 guide says not to turn them on just to finish one. Since 16.4, the Next.js blog recommends them for every app, new apps from create-next-app have them on, and the blog says they'll be the default in Next.js 17. So it's worth doing, as a step of its own:

Playground: turning on Cache Components
The upgraded tea shop, one step at a timeNext.js
app/teas/[id]/
next.config.mjs
/** @type {import('next').NextConfig} */const nextConfig = {  cacheComponents: true,}; export default nextConfig;
next build failed · Next.js 16.4.3
$ next build▲ Next.js 16.4.3 (Turbopack)⚠ `cacheComponents` is enabled without a corresponding `partialPrefetching` option. Set `partialPrefetching` to either `true` or `false`. The only reason to set `partialPrefetching` to `false` is if you're migrating an older Cache Components app. The initial release of Cache Components did not include Partial Prefetching. New projects should enable both Cache Components and Partial Prefetching. Both Cache Components and Partial Prefetching will be enabled everywhere in the next major release, and the old configurations will be removed. Learn more: https://nextjs.org/docs/app/guides/adopting-partial-prefetching- Cache Components enabled   Creating an optimized production build ...✓ Compiled successfully  Running TypeScript ...  Finished TypeScript ...  Collecting page data ...Error: Route "/teas/[id]": Next.js encountered uncached or runtime data during prerendering. `fetch(...)`, `cookies()`, `headers()`, `params`, `searchParams`, or `connection()` accessed outside of `<Suspense>` prevents the route from being prerendered, blocking the page load and leading to a slower user experience. Ways to fix this:  - [stream] Provide a placeholder with `<Suspense fallback={...}>` around the data access  - [cache] For uncached data (`fetch`, database calls): cache the access with `"use cache"` (does not apply to `connection()`)  - [block] Set `export const instant = false` to allow a blocking route Learn more: https://nextjs.org/docs/messages/blocking-prerender-dynamic    at body (<anonymous>)    at html (<anonymous>)To get a more detailed stack trace and pinpoint the issue, try one of the following:  - Start the app in development mode by running `next dev`, then open "/teas/[id]" in your browser to investigate the error.  - Rerun the production build with `next build --debug-prerender` to generate better stack traces.Error occurred prerendering page "/teas/[id]". Read more: https://nextjs.org/docs/messages/prerender-errorExport encountered an error on /teas/[id]/page: /teas/[id], exiting the build.⨯ Next.js build worker exited with code: 1 and signal: null
Try it yourself
Only cacheComponents

Line 3 turns Cache Components on. Next.js 16.4.3 warns that partialPrefetching isn't set, then stops at /teas/[id]: the page reads params and a cookie outside Suspense.

Every step was built with next build from Next.js 16.4.3; the Terminal shows the codemod's diff, when it ran, and what the build printed. Reset goes back to the first step.

Only cacheComponents: true brings two messages. A warning: Next.js 16.4.3 wants partialPrefetching set too, and new apps set both. And an error at /teas/[id], which reads params and a cookie outside Suspense. The docs offer a codemod, cache-components-instant-false, that adds export const instant = false to every page and layout, so the build passes while you fix routes one by one. The recording shows its diff. It's an opt-out, not a fix: the tea page still waits for everything. The last step is the fix:

app/teas/[id]/page.tsx, made for Cache ComponentsTSX
import { Suspense } from "react";import { notFound } from "next/navigation";import { getCurrency } from "../../currency";import { teas } from "../../teas"; export function generateStaticParams() {  return teas.map((tea) => ({ id: tea.id }));} export async function generateMetadata(props: { params: Promise<{ id: string }> }) {  const params = await props.params;  const tea = teas.find((item) => item.id === params.id);  return { title: tea ? tea.name : "Tea shop" };} export default async function TeaPage(props: { params: Promise<{ id: string }> }) {  const params = await props.params;  const tea = teas.find((item) => item.id === params.id);  if (!tea) notFound();  return (    <main>      <h1>{tea.name}</h1>      <Suspense fallback={<p>{tea.price}</p>}>        <Price amount={tea.price} />      </Suspense>    </main>  );} async function Price({ amount }: { amount: number }) {  return (    <p>      {amount} {await getCurrency()}    </p>  );} 

Lines 6 to 8 list the teas, so the build knows their params. Lines 23 to 25 put the part that reads the cookie in Suspense, with the price as its fallback. Price, on lines 30 to 36, awaits getCurrency(). The build printed ◐ /teas/green and ◐ /teas/mint: a shell with the name and the price, and the currency streamed in for each request. Cache Components and Partial Prerendering teach the model, and the production checklist searches for leftover opt-outs.

Upgrading with a coding agent

A coding agent is an AI assistant that edits your project and runs commands. Next.js 16.4 added a mode of next upgrade for one, which its docs mark Experimental. This part is from the docs and wasn't recorded: it needs the network, and an agent.

The upgrade command, three ways (from the docs)Shell
# Next.js 16.1 and later: install the latest version and run its codemods
npx next upgrade
# Before 16.1, the same command from @next/codemod
npx @next/codemod@canary upgrade latest
# Hand the upgrade to a coding agent, with the newest upgrade tooling
npx next@canary upgrade --agent=latest

Line 2 is the upgrade command of Next.js 16.1 and later: it installs the newest version and offers its codemods. Before 16.1, line 4 does the same from @next/codemod; the tea shop on 14 would start there. Line 6 hands the upgrade to an agent. The docs describe what it does:

  • It picks the target from a policy. security, the default: for a stable version that a known vulnerability affects, the latest safe release in the earliest eligible major. latest: npm's latest stable release for stable, release candidate, beta and preview apps, and the latest canary for a canary app. experimental-future: latest, then the applicable Future Defaults, features Next.js plans to turn on by default; for App Router apps on 16.3 or later, it currently prepares the Cache Components guide and its adoption Skill.
  • It copies the upgrade guide of every major it crosses and the codemod commands into a task, and asks the agent to work in a separate git worktree, run the project's checks and verify the result.
  • From a terminal, it can start an installed Codex or Claude Code session, or copy the task for another agent. Inside an agent, it prints the task for that agent.
  • next@canary runs the newest upgrade tooling, even for an app on an older version.

Since 16.4, next dev and next build can also remind you when an upgrade is available. The reminders are on by default, with the security policy; experimental.agentUpgrade picks another, and false turns them off:

next.config.ts: upgrade reminders for security releasesTSX
import type { NextConfig } from "next"; const nextConfig: NextConfig = {  experimental: {    agentUpgrade: "security",  },}; export default nextConfig; 

Line 5 asks for reminders about releases that fix vulnerabilities in the installed version. Agents also read docs: Next.js ships the docs for each version in node_modules/next/dist/docs/, and an AGENTS.md file can point the agent there, so it follows the guide for the version you have, not the one it remembers. Debugging shows the MCP endpoint that lets an agent read the dev server's errors.

An agent's upgrade is checked like yours

An agent runs the same codemods and reads the same guides. Read its diff, as you read the codemod's above, and let the same build, tests and preview prove the result.

A safe upgrade, step by step

Here is the tea shop's upgrade by hand, on a branch of its own:

The tea shop's upgrade, one command at a timeShell
git switch -c upgrade-next-16
npm install next@16.4.3 react@19.3.0 react-dom@19.3.0
npx @next/codemod@16.4.3 next-async-request-api .
git add -A && git commit -m "Await request APIs"
npx @next/codemod@16.4.3 middleware-to-proxy .
git add -A && git commit -m "Rename middleware.ts to proxy.ts"
npm run build
npx vitest run && npx playwright test

Line 1 makes a branch, so the main branch keeps working. Line 2 installs Next.js 16.4.3 and the React it comes with. Lines 3 and 5 run one codemod each, with the version pinned, and lines 4 and 6 commit after each, so each diff can be read on its own. Line 7 builds, which also type-checks. Line 8 runs the unit tests, then the end-to-end tests (Testing components, End-to-end testing). The docs' guides use @next/codemod@canary, which has the newest codemods; a pinned version keeps the steps the same every time you run them. Between lines 5 and 7, fix what the codemods marked, and anything the guide lists that your app uses.

Six steps joined by arrows: new branch, read the guides, codemods, then down to build, tests and e2e, and preview deploy. Only after the preview is the branch merged.new branchread the guidescodemodsbuildtests and e2epreview deploythen merge
The upgrade happens on its own branch. Each step checks the one before it: the build checks the codemods, the tests check what the build can't, and a preview deploy checks the app where it will run. Then you merge.

Then deploy the branch to a preview, a copy of the app at its own address, and check it the way you'd check a launch, with the production checklist. Some changes a codemod makes for you, and some you have to make. Sort them:

A codemod, or you?
  • Rename middleware.ts to proxy.ts.
  • Make a page await props.params.
  • Make getCurrency() await cookies(), and its callers await it.
  • Replace next lint in package.json's scripts.
  • Replace a custom webpack function.
  • Decide what replaces experimental.dynamicIO.
  • Remove export const experimental_ppr from pages.
  • Put the currency in Suspense so the shell can be prerendered.
Try it yourself
0 of 8 correct

Sort each change: does a codemod make it, or do you?

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

  • "An upgrade is just npm install next@latest." No. The old shop installed in a second and didn't build. The code has to follow what the new version changed.
  • "A codemod fixes everything." No. It changed the page and left @next-codemod-error in app/currency.ts, and the build refused until that was fixed by hand.
  • "Deprecated means it no longer works." No. middleware.ts still ran, with a warning. It means it will be removed later, so change it now.
  • "If it builds, it works." No. The JavaScript page built without an error and answered 500. Tests and a preview catch what a build can't.
  • "Upgrading to 16 turns on Cache Components." No. It's cacheComponents: true, a change of its own, and it brought new build errors.
  • "Canary is the newest, so it's the best for production." No. Canaries come out daily, and the support policy says not to serve production traffic from them.
  • "An agent's upgrade needs no review." No. Read its diff and run the same checks, as for your own.

Practice exercises

These exercises are about Next.js projects, which can't run inside this page. Answer first, then use the hints.

Exercise 1 · Warm-upRead a deprecation warning

The old shop's build printed this. Which command does it suggest?

Starter codeText
⚠ The "middleware" file convention is deprecated. Please use "proxy" instead.

  To migrate automatically, run:
  npx @next/codemod@canary middleware-to-proxy .

  Learn more: https://nextjs.org/docs/messages/middleware-to-proxy

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upMajor, minor or patch?

    A shop moves from Next.js 16.3.6 to 16.4.3. Is that a major, minor or patch upgrade?

    Starter codeText
    16.3.6 → 16.4.3

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticePredict the codemod

      next-async-request-api renames the prop to props.params. Which word does it put in front of props.params?

      Starter codeTSX
      import { notFound } from "next/navigation";
      import { getCurrency } from "../../currency";
      import { teas } from "../../teas";
      
      export function generateMetadata({ params }: { params: { id: string } }) {
        const tea = teas.find((item) => item.id === params.id);
        return { title: tea ? tea.name : "Tea shop" };
      }
      
      export default function TeaPage({ params }: { params: { id: string } }) {
        const tea = teas.find((item) => item.id === params.id);
        if (!tea) notFound();
        return (
          <main>
            <h1>{tea.name}</h1>
            <p>
              {tea.price} {getCurrency()}
            </p>
          </main>
        );
      }
      

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeFix what the codemod left

        The codemod couldn't fix this helper. To await cookies() in it, which keyword does the declaration on line 3 need?

        Starter codeTSX
        import { cookies } from "next/headers";
        
        export function getCurrency() {
          return cookies().get("currency")?.value ?? "USD";
        }
        

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeRead the config error

          One option in this next.config.mjs stopped next build. What replaced it?

          Starter codeJavaScript
          /** @type {import('next').NextConfig} */
          const nextConfig = {
            eslint: { ignoreDuringBuilds: true },
            serverRuntimeConfig: { supplierKey: process.env.SUPPLIER_KEY },
            experimental: { dynamicIO: true },
          };
          
          export default nextConfig;
          

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengePredict the route's symbol

            With Cache Components on, what does next build print for /teas/mint?

            Starter codeTSX
            import { Suspense } from "react";
            import { notFound } from "next/navigation";
            import { getCurrency } from "../../currency";
            import { teas } from "../../teas";
            
            export function generateStaticParams() {
              return teas.map((tea) => ({ id: tea.id }));
            }
            
            export async function generateMetadata(props: { params: Promise<{ id: string }> }) {
              const params = await props.params;
              const tea = teas.find((item) => item.id === params.id);
              return { title: tea ? tea.name : "Tea shop" };
            }
            
            export default async function TeaPage(props: { params: Promise<{ id: string }> }) {
              const params = await props.params;
              const tea = teas.find((item) => item.id === params.id);
              if (!tea) notFound();
              return (
                <main>
                  <h1>{tea.name}</h1>
                  <Suspense fallback={<p>{tea.price}</p>}>
                    <Price amount={tea.price} />
                  </Suspense>
                </main>
              );
            }
            
            async function Price({ amount }: { amount: number }) {
              return (
                <p>
                  {amount} {await getCurrency()}
                </p>
              );
            }
            

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, think about what the new version changed, and who changes the code: a codemod, or you.

              The Upgrading Next.js quiz · 7 questionsScore: first tries count
              1. Question 1 of 7Next.js goes from 15.5 to 16.0. What does the first number changing tell you?

                Choose an answer to see the explanation.

              2. Question 2 of 7What did next-async-request-api do to export default function TeaPage({ params }: { params: { id: string } })?

                Choose an answer to see the explanation.

              3. Question 3 of 7Why did the codemod leave @next-codemod-error in app/currency.ts instead of fixing it?

                Choose an answer to see the explanation.

              4. Question 4 of 7The shop still has middleware.ts after moving to Next.js 16.4.3. What happens?

                Choose an answer to see the explanation.

              5. Question 5 of 7Why did next build stop for the next.config.mjs with a webpack function?

                Choose an answer to see the explanation.

              6. Question 6 of 7Does moving to Next.js 16 turn on Cache Components?

                Choose an answer to see the explanation.

              7. Question 7 of 7Which order keeps an upgrade safe?

                Choose an answer to see the explanation.

              Key takeaways

              • A major can break code; read its upgrade guide, and the guide of every major you cross. Patches and minors shouldn't, except fixes for a major in Maintenance LTS, which the support policy ships as minors even when they break something.
              • Changes are announced on nextjs.org/blog, in the upgrade guides, on GitHub releases, and as warnings in your terminal.
              • Run codemods on a clean branch, one at a time, and read each diff. Fix what they mark with @next-codemod-error.
              • Since 15, params, searchParams, cookies(), headers() and draftMode() return promises; 16 removed the old way.
              • 16 renamed middleware.ts to proxy.ts, made Turbopack the default, and removed old options. Cache Components are a separate step.
              • A build isn't enough: tests, a preview deploy and the production checklist prove the upgrade, yours or an agent's.

              Remember the one-liner.
              Read what changed, let codemods do what they can, fix the rest by hand, and prove it before you merge.

              Coming next: Structuring a large app. It organizes routes, features, data access and UI so a growing app, like the tea shop after its upgrade, stays easy to change.

              CompleteFrontend Clear concepts. Working examples.