What’s Changed & Upgrading

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Next.js 16.3 (the latest release on 2026-10-03 is 16.3.8, and the official documentation reports @doc-version 16.3.8), React 19.3 and Node.js 20.9 or later, verified on 2026-10-03. It covers the App Router and the Pages Router: the change log applies to both, the code examples are App Router code unless a Pages Router difference is called out, and the caching entries label the two models, the previous model (the default, with cacheComponents off) and Cache Components (cacheComponents: true, opt-in). The upgrade commands, the codemod output, the build errors and the curl results below were produced with Next.js 16.3.8 and Node.js 24.16 on macOS in scratch projects (an older 15.5 project that was upgraded, a Create React App style single-page app, and a copy of the Bookshelf catalogue that the rest of this section grows).

Most Next.js tutorials, answers and books were written for a version that no longer exists. Two books were used while preparing this section, and they are about Next.js 12 and 13 and the Pages Router; this page is where the differences between that world and the current one are written down once, so the other pages of the section can stay focused on how things work today. It has four parts: a change log by major version, the tools that automate an upgrade, the two official migration guides for single-page apps, and the collected lists of everything in the books that is outdated or wrong. The concepts themselves are explained in depth on the pages linked from each entry, for instance Caching, Redirects, Rewrites & Proxy and Turbopack & the Compiler.

The Version Baseline

Table 1. Version baseline of this section, verified on 2026-10-03
Item Value

Next.js

16.3 (16.3.0 released on 2026-08-03; the latest patch is 16.3.8, a security release of 2026-09-30). Earlier 16.x releases: 16.0 on 2025-10-21, 16.1 on 2025-12-18 and 16.2 on 2026-03-18. The canary channel is on 16.4.

Support policy

16.x is Active LTS and 15.x is Maintenance LTS (backports reach 15.5.27); 14.x and older are unsupported, so security fixes do not reach them. See the support policy.

React

React 19.3 is the latest react on npm (2026-09-09). The App Router uses the React canary bundled with Next.js, which already includes the 19.2 features (Activity, useEffectEvent, ViewTransition). The Pages Router still supports React 18.

Requirements

Node.js 20.9 or later (Node.js 18 is no longer supported), TypeScript 5.1 or later, Chrome, Edge and Firefox 111+, Safari 16.4+.

Opt-in today, planned as future defaults

cacheComponents (Cache Components, 'use cache' and Partial Prerendering) and partialPrefetching (Instant Navigations, 16.3). The 16.3 release post says the behaviours behind Instant Navigations "will become the default in a future major version". Pages in this section teach both caching models and label them.

The Change Log: 13, 14, 15 and 16.3

Next.js has a major version about once a year, and each one has changed what "the normal way" looks like. The table is the map; each version then gets a section with the breaking changes and where the topic is covered.

Table 2. Next.js majors and the minors that matter, with release dates
Version Date Headline

13.0

2022-10-25

The app directory in beta (layouts, Server Components, streaming), Turbopack alpha, new next/image, next/link without a child <a>, @next/font. Minimum React 18.2 and Node.js 14.6.

13.4

2023-05-04

The App Router is stable. Turbopack in beta for dev, Server Actions in alpha.

14.0

2023-10-26

Server Actions stable, Partial Prerendering as a preview, Turbopack improvements. Minimum Node.js 18.17; next export removed.

14.2

2024-04-11

Turbopack for development as a release candidate, staleTimes (experimental), React 19 support in the release candidate.

15.0

2024-10-21

React 19, async request APIs, caching off by default for fetch, GET Route Handlers and the client router cache, Turbopack for development stable, next.config.ts, the @next/codemod upgrade CLI.

15.1

2024-12-10

React 19 stable, after stable, forbidden and unauthorized as experiments.

15.5

2025-08-18

Turbopack builds in beta, Node.js Middleware stable, typed routes stable, next typegen, next lint deprecated and deprecation warnings for what 16 removes.

16.0

2025-10-21

Turbopack default for dev and build, Cache Components, proxy.ts, synchronous request APIs removed, React Compiler stable, DevTools MCP, new caching APIs, AMP, next lint and runtime config removed.

16.1

2025-12-18

Turbopack file-system cache for next dev stable, next upgrade, next experimental-analyze, next dev --inspect.

16.2

2026-03-18

Hydration diff indicator, Adapters stable, Server Function logging, next start --inspect, bundled docs and AGENTS.md for coding agents, unstable_catchError.

16.3

2026-08-03

Instant Navigations (opt-in), less dev memory, build file-system cache by default, catchError, next/root-params, import.meta.glob, TypeScript 7 type checking.

Source: the Next.js release posts for 13, 13.4, 14, 14.2, 15, 15.1, 15.5, 16, 16.1, 16.2 and 16.3 (listed in the references)

Next.js 13 and 13.4: the App Router Arrives

Next.js 13 is the version the Pages Router books stop at. It introduced the app directory in beta with layouts, React Server Components and streaming, and the App Router was declared stable in 13.4 (May 2023). The release notes list the first set of breaking changes that tutorials from 2021 and 2022 trip over:

  • the minimum React version became 18.2 and the minimum Node.js version 14.6;

  • next/image was renamed next/legacy/image and the new image component took the name next/image;

  • a next/link child can no longer be an <a> (the temporary legacyBehavior prop keeps the old form);

  • the target option of next.config.js was removed, Internet Explorer was dropped as a target, and swcMinify became true;

  • @next/font replaced the CSS font loading recipes (13.2 made it built in as next/font).

The Pages Router was not removed. Both routers coexist in one application, and the Pages Router remains supported; it is simply no longer where the new features land. The pages of this section that touch it say so where it differs.

Source: Next.js 13 and Next.js 13.4

Next.js 14: Server Actions and next export

Next.js 14 (2023-10-26) is a small upgrade guide. Its summary is four lines:

  • the minimum Node.js version moved from 16.14 to 18.17;

  • the next export command was removed in favour of output: 'export' in next.config;

  • ImageResponse moved from next/server to next/og;

  • the @next/font package was removed in favour of the built-in next/font.

Server Actions became stable, and Partial Prerendering appeared as a preview. Version 14.2 added the first staleTimes experiment for the client router cache, which Next.js 15 used to change a default (see below).

Running next export in a current project shows what the removal looks like.

next export in Next.js 16.3.8
$ npx next export
⨯
    `next export` has been removed in favor of 'output: export' in next.config.js.
Learn more: https://nextjs.org/docs/app/building-your-application/deploying/static-exports

Next.js 15: Async Request APIs and Caching Off by Default

Version 15 (2024-10-21) made the two changes that break the most old code. Both are described in the version 15 upgrade guide.

Request-time APIs became asynchronous. cookies, headers, draftMode, the params prop of layout, page, route, default and the metadata image files, and the searchParams prop of page are Promises. Version 15 kept a temporary synchronous compatibility mode that logged a warning in development; Next.js 16 removed it. The reason is rendering: a Promise lets Next.js prerender everything before the first access to request data, which is what Partial Prerendering and Cache Components build on.

Version 15 and later: awaiting the request APIs
import { cookies } from 'next/headers'

// Before (13 and 14)
const cookieStore = cookies()
const token = cookieStore.get('token')

// After (15, required in 16)
const cookieStore = await cookies()
const token = cookieStore.get('token')

The page-level form is the one tutorials get wrong most often, because the destructured argument is now a Promise object:

A page before and after (the output of the codemod shown in a later section)
// Before
type Props = { params: { id: string }; searchParams: { lang?: string } }

export default function Page({ params, searchParams }: Props) {
  const theme = cookies().get('theme')?.value ?? 'light'
  /* ... */
}

// After
type Props = {
  params: Promise<{ id: string }>
  searchParams: Promise<{ lang?: string }>
}

export default async function Page(props: Props) {
  const searchParams = await props.searchParams
  const params = await props.params
  const theme = (await cookies()).get('theme')?.value ?? 'light'
  /* ... */
}

A synchronous Client Component page cannot await; it reads the Promise with React’s use hook instead (const params = use(props.params)). Next.js 15.5 added npx next typegen, which generates the global PageProps, LayoutProps and RouteContext helpers so that these types need not be written by hand; they are used throughout this section as PageProps<'/[locale]/books/[id]'>.

next typegen in the scratch Bookshelf app
$ npx next typegen
Generating route types...
✓ Types generated successfully

Caching defaults moved to "uncached". In versions 13 and 14 a fetch was cached by default, a GET Route Handler was cached unless it used a dynamic function, and the client router cache reused page segments for about 30 seconds. Version 15 reversed all three: fetch requests and GET handlers are not cached unless asked to be (cache: 'force-cache', export const dynamic = 'force-static'), and the client router cache no longer reuses page segments on navigation (staleTimes.dynamic is 0), although it still reuses them for back and forward navigation and for shared layouts. The full account of the layers, in both models, is on the Caching page; the shape of the change is this:

Table 3. Caching defaults across versions (previous model)
Behaviour Next.js 13 and 14 Next.js 15 and later

fetch without options

Cached (force-cache)

Not cached

GET Route Handler

Cached when it uses no dynamic function

Not cached; opt in with dynamic = 'force-static'

Client router cache for page segments

About 30 seconds for dynamic pages

Not reused (staleTimes.dynamic: 0); opt in with experimental.staleTimes

Other breaking changes in 15: the minimum Node.js version became 18.18; React 19 became the minimum for the App Router (the Pages Router accepts React 18 or 19); useFormState was replaced by useActionState; next/dynamic with ssr: false is disallowed in Server Components; the runtime value experimental-edge is an error (use edge); revalidateTag and revalidatePath throw if called during render; geo and ip were removed from NextRequest; Speed Insights auto-instrumentation was removed; experimental.bundlePagesExternals became bundlePagesRouterDependencies and experimental.serverComponentsExternalPackages became serverExternalPackages. Version 15 also made next.config.ts a supported file, shipped <Form>, stabilised instrumentation.js, and improved hydration error messages. Version 15.1 added after (stable) and forbidden/unauthorized (experimental).

Source: Next.js 15 and Next.js 15.1

Next.js 16.0: Turbopack, Proxy and the End of the Compatibility Period

Version 16 (2025-10-21) is the largest cleanup since 13. Its upgrade guide is long, but the changes group into five themes.

Turbopack by default. next dev and next build use Turbopack; the --turbopack flag in package.json scripts is no longer needed. A project with a custom webpack function and no turbopack key fails next build on purpose, so that nobody ships a build that silently ignores their webpack configuration. The three ways out are --webpack to opt out, --turbopack to build anyway, or an (empty or real) turbopack key. The turbopack configuration also moved out of experimental.

A webpack function without a turbopack key, built with Next.js 16.3.8
$ npx next build
▲ Next.js 16.3.8 (Turbopack)

  Creating an optimized production build ...
⨯ ERROR: This build is using Turbopack, with a `webpack` config and no `turbopack` config.
   This may be a mistake.

   As of Next.js 16 Turbopack is enabled by default and
   custom webpack configurations may need to be migrated to Turbopack.
   [...]
   `--turbopack` or `--webpack` flag explicitly or simply setting an
   empty turbopack config in your Next config file (e.g. `turbopack: {}`).

The moved key is detected by the configuration validator. This is the project that the upgrade codemod (below) had just processed, whose next.config.mjs still had the Next.js 15 shape.

experimental.turbopack after the upgrade, built with Next.js 16.3.8
▲ Next.js 16.3.8 (Turbopack)
⚠ Invalid next.config.mjs options detected:
⚠     Unrecognized key(s) in object: 'turbopack' at "experimental"
⚠ See more info here: https://nextjs.org/docs/messages/invalid-next-config
The fix: turbopack at the top level
// next.config.mjs
export default {
  turbopack: {}, // was: experimental: { turbopack: {} }
}

Synchronous request APIs removed. The version 15 compatibility mode is gone, so code that still reads params, searchParams, cookies(), headers() or draftMode() synchronously fails to type-check and fails at runtime. In Next.js 16 the props of opengraph-image, twitter-image, icon, apple-icon and sitemap (the id) became Promises as well.

A synchronous page and a one-argument revalidateTag, after the upgrade, built with Next.js 16.3.8
  Running TypeScript ...
app/books/[id]/page.tsx(6,27): error TS2339: Property 'get' does not exist on type 'Promise<ReadonlyRequestCookies>'.
lib/cache.ts(15,3): error TS2554: Expected 2 arguments, but got 1.
Failed to type check.

Caching APIs. revalidateTag takes a second argument, a cacheLife profile, and the single-argument form is deprecated (a TypeScript error, as above); updateTag is new, for Server Actions only, and gives read-your-writes semantics; refresh refreshes the client router from a Server Action; cacheLife and cacheTag are stable, so the unstable_ prefix and the aliased imports go away. experimental.ppr, the experimental_ppr segment export, experimental.dynamicIO and experimental.useCache were removed in favour of the top-level cacheComponents flag. Enabling cacheComponents is not a rename: it can surface build errors for uncached data outside <Suspense>, and the migration path is the Migrating to Cache Components guide. The mechanics are on the Caching and Partial Prerendering & Cache Components pages.

The three caching changes in code
// Before
import {
  unstable_cacheLife as cacheLife,
  unstable_cacheTag as cacheTag,
} from 'next/cache'
revalidateTag('posts')

// After (16): stable names, a profile on revalidateTag
import { cacheLife, cacheTag, revalidateTag } from 'next/cache'
revalidateTag('posts', 'max') // stale-while-revalidate

// For read-your-writes inside a Server Action
import { updateTag } from 'next/cache'
updateTag(`user-${userId}`)

A 'use server' file that calls updateTag, refresh and the two-argument revalidateTag compiled and type-checked with next build in the scratch project.

The fixed one-argument call, exercised in a Route Handler of the scratch Bookshelf app (POST /api/revalidate calling revalidateTag("books", "max")), after next build and next start -p 3903:

{"revalidated":true}

Source: revalidateTag

middleware became proxy. The file is proxy.ts, the exported function is proxy, and the runtime is Node.js and cannot be changed; the Edge runtime is not supported in the Proxy, so projects that need it keep middleware.ts. The flags that contained the word "middleware" were renamed (skipMiddlewareUrlNormalize is now skipProxyUrlNormalize).

middleware.ts renamed to proxy.ts by the middleware-to-proxy codemod
diff --git a/middleware.ts b/proxy.ts
similarity index 83%
rename from middleware.ts
rename to proxy.ts
--- a/middleware.ts
+++ b/proxy.ts
@@ -1,6 +1,6 @@
 import { NextResponse, type NextRequest } from 'next/server'

-export function middleware(request: NextRequest) {
+export function proxy(request: NextRequest) {
   if (request.nextUrl.pathname === '/old-shelf') {
     return NextResponse.redirect(new URL('/books', request.url))
   }

The renamed file was built and started with next start -p 3903; curl -I http://localhost:3903/old-shelf answered 307 Temporary Redirect with location: /books, and the build’s route table printed ƒ Proxy (Middleware). The behaviour of the Proxy (matcher, order of evaluation, what it should and should not do) is on the Redirects, Rewrites & Proxy page.

Removals. These were deprecated earlier and are gone:

  • AMP: the amp configuration, next/amp and useAmp, and export const config = { amp: true };

  • next lint and the eslint option of next.config; next build no longer lints;

  • serverRuntimeConfig and publicRuntimeConfig (use environment variables);

  • the devIndicators options appIsrStatus, buildActivity and buildActivityPosition;

  • experimental.dynamicIO, experimental.useCache, experimental.ppr and experimental_ppr;

  • unstable_rootParams (replaced by next/root-params, which shipped in 16.3);

  • the size and First Load JS columns of the next build output, which were inaccurate with Server Components.

Each removal has a visible failure mode. Importing next/amp, setting a removed key, or running the old command gives these messages in 16.3.8.

Three removals, as the 16.3.8 toolchain reports them
$ npx next lint
Invalid project directory provided, no such directory: /path/to/project/lint

./pages/amp.tsx:1:1
Error: Module not found: Can't resolve 'next/amp'

⚠ `eslint` configuration in next.config.mjs is no longer supported.
⚠ Invalid next.config.mjs options detected:
⚠     Unrecognized key(s) in object: 'serverRuntimeConfig', 'eslint'
Error: 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.

The replacement for serverRuntimeConfig and publicRuntimeConfig is plain environment variables: read secrets on the server, prefix only genuinely public values with NEXT_PUBLIC_, and call connection() before reading process.env when a value must be read at request time rather than inlined at build time (see Configuration, TypeScript & Environment Variables).

Behaviour changes and defaults. These do not break compilation but change output:

  • Parallel routes need default.js. Every parallel route slot needs a default.js (return null or call notFound()). In the scratch test a slot without one built successfully, and the request that did not match the slot answered 404; adding default.tsx returning null made the same request answer 200. The upgrade guide says builds fail without it, so treat the file as required (see Parallel & Intercepting Routes).

  • next/image defaults. minimumCacheTTL went from 60 seconds to 4 hours; 16 left the default imageSizes; qualities defaults to [75]; local IPs are blocked unless dangerouslyAllowLocalIP is set; maximumRedirects defaults to 3; local images with a query string need localPatterns with a search entry. next/legacy/image and images.domains are deprecated; use next/image and remotePatterns. See Images, Fonts & Scripts.

  • Dev and build directories. next dev writes to .next/dev, so dev and build can run at the same time, and a lockfile prevents two next dev or two next build processes on one project.

  • ESLint flat config. @next/eslint-plugin-next defaults to the flat config format.

  • Scroll behaviour. Next.js no longer overrides a global scroll-behavior: smooth during navigation; add data-scroll-behavior="smooth" to <html> to get the old override.

  • Navigation. The prefetch and navigation layer was rewritten (layout deduplication, incremental prefetching): more, smaller prefetch requests with no code changes.

  • next dev config load. The config file is no longer loaded by the next dev launcher, so a check such as process.argv.includes('dev') in next.config.js returns false; check NODE_ENV or the config phase instead.

  • Sass. sass-loader 16 and the modern Sass API; Turbopack does not support the legacy ~ prefix in Sass imports.

Image configuration keys of Next.js 16, accepted by the validator in 16.3.8
// next.config.mjs
export default {
  turbopack: {},
  images: {
    remotePatterns: [
      { protocol: 'https', hostname: 'covers.example.com', pathname: '/books/**' },
    ],
    localPatterns: [{ pathname: '/assets/**', search: '?v=1' }],
    qualities: [50, 75, 100],
    minimumCacheTTL: 60, // the pre-16 default; the new default is 14400 (4 hours)
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], // 16 restored
  },
}
A parallel route slot with its default.tsx
// app/@modal/default.tsx
export default function Default() {
  return null
}

Also in 16.0. React 19.2 features arrive through the bundled canary (Activity, useEffectEvent, ViewTransition); the React Compiler option reactCompiler is stable but off by default; the Next.js DevTools MCP server lets coding agents inspect a running app; unstable_ prefixes were dropped from stabilised APIs; and the first alpha of the Build Adapters API appeared (stable in 16.2). Link with legacyBehavior was announced for removal in 16 by the 15.5 deprecation notices, but a page using <Link passHref legacyBehavior><a>…</a></Link> still built without an error on Next.js 16.3.8 in the scratch project. It is deprecated, not working-by-design: remove it with the new-link codemod. The tools for all of this are in the next sections; the details of Turbopack and the compiler are on Turbopack & the Compiler.

Source: Next.js 16 and Next.js 15.5

Next.js 16.1, 16.2 and 16.3: The Minors

The minor releases of 16 carry no breaking change for a project already on 16.0, but several of them change behaviour by default, and several are experiments whose names changed between minors. Anyone reading a post or a prompt from one month may find the name different in the next.

Table 4. What each 16.x minor added, and what was still experimental or opt-in
Version Added Status to check

16.1 (2025-12-18)

Turbopack file-system caching for next dev is stable and on by default; the next upgrade command; next experimental-analyze (an interactive bundle analyzer for Turbopack); next dev --inspect; Turbopack externalises transitive serverExternalPackages; the DevTools MCP gains a get_routes tool.

next experimental-analyze is still labelled experimental. @next/bundle-analyzer works with webpack only.

16.2 (2026-03-18)

Faster dev start-up and rendering, a new default error page, Server Function logging in the dev terminal, the hydration diff indicator (a + Client / - Server legend in the error overlay), next start --inspect, the transitionTypes prop on Link, error causes in the overlay, multiple icon formats, bundled docs in node_modules/next/dist/docs/ and an AGENTS.md in new projects, Adapters stable (adapterPath at the top level).

unstable_catchError and unstable_retry (experimental). experimental.prefetchInlining and experimental.cachedNavigations appeared as flags.

16.3 (2026-08-03)

Up to 90 percent less memory for next dev (Turbopack memory eviction), the Turbopack file-system cache for next build, both on by default; TypeScript 7 type checking when the project’s typescript is version 7; native Node.js streams for rendering; prefetch inlining without a flag; immutable static assets reusable across deployments; catchError and retry (stable); import.meta.glob in Turbopack; next/root-params; next dev writes and maintains a version-matched AGENTS.md block; Instant Navigations (Instant Insights, Navigation Inspector, the instant() helper from @next/playwright, ISR with an app shell).

Opt-in: cacheComponents and partialPrefetching. Experimental: the Rust React Compiler and experimental.useOffline (network resilience).

A few of these renamed or moved between minors, and the names matter when following older material:

  • catchError. In 16.2 it shipped as unstable_catchError() with an unstable_retry() callback (and an unstable_retry prop for error.tsx). In 16.3 the stable names are catchError and retry, imported from next/error. The remove-unstable-prefix codemod (checked in its source) renames only unstable_cacheLife and unstable_cacheTag, so rename this one by hand.

  • next/root-params. Version 16.0 removed unstable_rootParams and promised an alternative in a minor release; that alternative is next/root-params, which arrived in 16.3. It works in Server Components (and inside use cache scopes) but not yet in Route Handlers or Server Actions.

  • Adapters. The alpha of 16.0 read experimental.adapterPath; 16.2 made adapterPath a stable top-level option.

  • Bundle analysis. next experimental-analyze (16.1) replaces @next/bundle-analyzer for Turbopack projects, and it is experimental by name and by status.

  • File-system caching. Dev caching was stable in 16.1; the build cache and memory eviction became defaults in 16.3 (see Turbopack & the Compiler).

  • Agents. The DevTools MCP is from 16.0 and get_routes from 16.1; bundled docs are from 16.2 and the AGENTS.md block that next dev maintains is from 16.3.

catchError in Next.js 16.3 (the 16.2 spelling was unstable_catchError and unstable_retry)
// app/my-error-boundary.tsx
'use client'
import { catchError, type ErrorInfo } from 'next/error'

function ErrorFallback(props: { title: string }, { error, retry }: ErrorInfo) {
  return (
    <div>
      <h2>{props.title}</h2>
      <p>{error instanceof Error ? error.message : String(error)}</p>
      <button onClick={() => retry()}>Try again</button>
    </div>
  )
}

export default catchError(ErrorFallback)

The snippet in the 16.3 post and in the catchError reference reads error.message directly; with the types shipped in 16.3.8 the error field of ErrorInfo is unknown, and that line failed next build with Property 'message' does not exist on type 'unknown' until it was narrowed as above. The page below the boundary was built and served (/es/posts/hello answered with es: hello from the root-params example and fine from the wrapped child).

next/root-params in Next.js 16.3, replacing the removed unstable_rootParams
// app/[lang]/posts/[slug]/page.tsx
import { lang } from 'next/root-params'

export default async function PostPage(props: PageProps<'/[lang]/posts/[slug]'>) {
  const { slug } = await props.params
  const language = await lang()
  return (
    <p>
      {language}: {slug}
    </p>
  )
}

The two opt-in flags of 16.3 are both off by default and can be turned on independently of an upgrade.

The 16.3 opt-ins
// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  cacheComponents: true, // Cache Components (use cache, Partial Prerendering)
  partialPrefetching: true, // Instant Navigations: reusable loading shells
}

export default nextConfig

A project with both flags on, a [lang] root layout and generateStaticParams built, and its route table showed ◐ (Partial Prerender) entries next to the ○ (Static) ones.

Tools for an Upgrade

next upgrade and the upgrade codemod

Since 16.1, next upgrade is the command. Versions before 16.1 do not have it and use the separate codemod package:

The two ways to run the upgrade
# Next.js 16.1 and later
npx next upgrade

# Any earlier version (and the form the 15 and 16 upgrade guides show)
npx @next/codemod@canary upgrade latest

next upgrade [directory] [--revision <revision>] [--verbose] takes a dist tag (latest, canary, rc, beta) or an exact version. The underlying upgrade codemod takes patch, minor (the default for stable versions) or major, a dist tag, or an exact version, and the flags --verbose and -y, --yes. It updates next, react and react-dom, asks which Next.js codemods to apply, and runs the React 19 codemods when the React major changes.

next upgrade --help in Next.js 16.3.8
Usage: next upgrade [directory] [options]

Upgrade Next.js apps to desired versions with a single command.

Options:
  --revision <revision>  Specify the target Next.js version using an NPM dist
                         tag (e.g. "latest", "canary", "rc", "beta") or an
                         exact version number (e.g. "15.0.0"). (default:
                         "latest")
  --verbose              Verbose output (default: false)
  -h, --help             Displays this message.

Run in a project that is already on the target version, it changes nothing.

npx next upgrade on a project already at 16.3.8
  Running in non-interactive mode. Every prompt will accept its default.
✓ Current Next.js version is already on the target version "v16.3.8".

To see an actual upgrade, a small project on Next.js 15.5.27 and React 19.0.0 was created with the old idioms (middleware.ts, synchronous params and cookies(), aliased unstable_cacheLife, a one-argument revalidateTag, experimental.turbopack, a next lint script) and upgraded with the documented command. The terminal was not interactive, so every prompt took its default, which is what --yes forces:

npx @next/codemod@canary upgrade latest --yes on the Next.js 15.5.27 project (trimmed)
  Running in non-interactive mode. Every prompt will accept its default.
  Detected installed versions:
  - React: v19.0.0
  - Next.js: v15.5.27
  Applying all codemods recommended for your upgrade:
    - middleware-to-proxy
    - remove-unstable-prefix
    - remove-experimental-ppr
    - cache-components-instant-false
    - remove-partial-prefetch
Upgrading your project to Next.js 16.3.8...
[... npm install ...]
✔ Codemods have been applied successfully.

Four results of that run are worth knowing before running it on a real project.

  • package.json was rewritten: next became 16.3.8, react and react-dom 19.3.0, eslint-config-next 16.3.8, and @types/react was pinned with an overrides entry. Commit the lockfile diff separately.

  • middleware.ts became proxy.ts with the function renamed, and unstable_cacheLife as cacheLife became plain cacheLife.

  • The cache-components-instant-false codemod, which the non-interactive run applied, inserts export const instant = false into every page, layout and default file so that Cache Components can be enabled route by route. Without cacheComponents: true that export is itself an error, as the next block shows. Accept that codemod only when adopting Cache Components, or delete the inserted lines.

  • The run did not migrate experimental.turbopack in next.config.mjs, did not replace the next lint script, did not add the second argument of revalidateTag, and did not make params or cookies() asynchronous. Those are separate codemods or manual edits.

The inserted opt-out, built without cacheComponents, with Next.js 16.3.8
./app/books/[id]/page.tsx:6:14
Error: Route segment config "instant" requires `nextConfig.cacheComponents` to be enabled.
> 6 | export const instant = false;

Individual codemods

npx @next/codemod <transform> <path> runs one transform; --dry makes a dry run and --print prints the changed output. Use --dry on a first attempt: in the scratch run --print on its own printed the new code and still wrote it to the file. The tool refuses to run in a dirty git working tree unless --force is passed (it stops with "please stash or commit your git changes"). A wrong path reports 0 ok instead of failing, so check the file count in the Results summary. The names below are the ones the codemods page lists; the ones marked with an asterisk were executed for this page and the others only confirmed in the list.

Table 5. Codemods that matter for the move from 13 to 16.3
Codemod Since What it does

next-async-request-api (*)

15.0

Makes cookies(), headers(), draftMode(), params and searchParams asynchronous, adding await, async or the use hook, and adds UnsafeUnwrapped casts where it cannot decide. Not part of the upgrade codemod’s list, so run it by hand.

app-dir-runtime-config-experimental-edge

15.0

Changes experimental-edge to edge in the runtime segment config.

next-request-geo-ip

15.0

Replaces geo and ip of NextRequest with @vercel/functions.

next-og-import (*)

14.0

Rewrites ImageResponse imports from next/server to next/og.

metadata-to-viewport-export

14.0

Moves viewport settings from the metadata export to the viewport export.

built-in-next-font (*)

13.2

Rewrites @next/font imports to next/font.

next-image-to-legacy-image

13.0

Rewrites next/image imports to next/legacy/image, to keep the old component while the code is migrated.

next-image-experimental

13.0

Migrates to the new image component (layout, objectFit and related props become styles and props of the new one).

new-link (*)

13.0

Removes the <a> child of Link and moves its attributes to Link.

remove-experimental-ppr

16.0

Removes the experimental_ppr segment export.

remove-unstable-prefix (*)

16.0

Drops the unstable_ prefix of stabilised APIs (cacheLife, cacheTag).

middleware-to-proxy (*)

16.0

Renames middleware.ts to proxy.ts, the function to proxy, and the renamed flags.

next-lint-to-eslint-cli (*)

16.0

Replaces next lint with the ESLint CLI.

cache-components-instant-false (*)

16.3

Adds export const instant = false to every route file so that cacheComponents can be enabled and the opt-outs removed route by route.

remove-partial-prefetch

16.3

Removes export const prefetch = 'partial' after enabling partialPrefetching globally.

agents-md

Guide

Sets up AGENTS.md with version-matched docs for coding agents (npx @next/codemod@canary agents-md).

cra-to-next

11

Bootstraps a migration of a Create React App project (see the next section for the current guide).

The three transforms of the 15 and 16 upgrades that a typical Pages Router era project needs are shown running below. The project had a synchronous page, a middleware.ts and an aliased unstable_cacheLife; the three commands were run one after another, committing between them.

npx @next/codemod@latest next-async-request-api . (summary line)
Executing command: jscodeshift --parser=tsx [...] --transform [...]/transforms/next-async-request-api.js .
Processing 3 files...
Results:
0 errors
2 unmodified
0 skipped
1 ok
The resulting diff of the three codemods (next-async-request-api, middleware-to-proxy, remove-unstable-prefix)
--- a/app/books/[id]/page.tsx
+++ b/app/books/[id]/page.tsx
@@ -1,9 +1,11 @@
 import { cookies } from 'next/headers'

-type Props = { params: { id: string }; searchParams: { lang?: string } }
+type Props = { params: Promise<{ id: string }>; searchParams: Promise<{ lang?: string }> }

-export default function Page({ params, searchParams }: Props) {
-  const theme = cookies().get('theme')?.value ?? 'light'
+export default async function Page(props: Props) {
+  const searchParams = await props.searchParams;
+  const params = await props.params;
+  const theme = (await cookies()).get('theme')?.value ?? 'light'

--- a/lib/cache.ts
+++ b/lib/cache.ts
@@ -1,6 +1,6 @@
 import {
-  unstable_cacheLife as cacheLife,
-  unstable_cacheTag as cacheTag,
+  cacheLife,
+  cacheTag,
   revalidateTag,
 } from 'next/cache'

The codemod left revalidateTag('books') alone; that one-argument call is the TypeScript error shown earlier, fixed by hand with revalidateTag('books', 'max'). After the manual fixes the upgraded project built and started:

Build route table of the upgraded project, then curl against next start -p 3903
Route (app)
┌ ○ /_not-found
└ ƒ /books/[id]

ƒ Proxy (Middleware)

$ curl -s -H 'Cookie: theme=dark' 'http://localhost:3903/books/1?lang=es'
<p>Book <!-- -->1<!-- --> in <!-- -->es<!-- --> (<!-- -->dark<!-- -->)</p>      (excerpt of the HTML)

$ curl -sI http://localhost:3903/old-shelf
HTTP/1.1 307 Temporary Redirect
location: /books

The older transforms behave the same way. On three tiny files (a @next/font/google import, an ImageResponse from next/server, and <Link><a className="nav">Books</a></Link>), built-in-next-font, next-og-import and new-link produced:

Diff produced by built-in-next-font, next-og-import and new-link
-import { Inter } from '@next/font/google'
+import { Inter } from "next/font/google"

-import { ImageResponse } from 'next/server'
+import { ImageResponse } from "next/og";

-    <Link href="/books">
-      <a className="nav">Books</a>
+    <Link href="/books" className="nav">
+      Books
     </Link>

The ESLint codemod does what it says: on the next lint project it created eslint.config.mjs (a flat config that spreads eslint-config-next/core-web-vitals and eslint-config-next/typescript) and changed the script to "lint": "eslint .".

npx @next/codemod@latest next-lint-to-eslint-cli .
Migrating from next lint to the ESLint CLI...
   Created eslint.config.mjs
   Updated script "lint": "next lint" → "eslint ."
Updated package.json scripts and dependencies

Migration complete! Your project now uses the ESLint CLI.

A safe order for an upgrade

The commands above combine into a routine that works for one major at a time. Skipping versions is possible (the 16 guide is written for 15), but each guide assumes the previous one was applied, and the two compatibility periods of Next.js (synchronous request APIs in 15, middleware in 16) are easiest to leave one at a time.

  1. Commit everything and create a branch; the codemods refuse to run on a dirty tree.

  2. Upgrade to the latest patch of the current major first (npx @next/codemod upgrade patch), so that the deprecation warnings of the current version are visible.

  3. Read the upgrade guide of the next major from top to bottom once; most steps are one paragraph.

  4. Run npx next upgrade (16.1+) or npx @next/codemod@canary upgrade latest, answer the prompts, and review the diff of package.json and the lockfile on its own.

  5. Run npx @next/codemod@latest next-async-request-api . when the project still reads request APIs synchronously, then npx next typegen and replace hand-written params types by PageProps, LayoutProps and RouteContext.

  6. Fix next.config by hand: turbopack at the top level, removed keys, images defaults, and the cacheComponents decision. Keep cacheComponents off until the rest builds.

  7. Run next build. Type errors and the validator messages shown in this page are the checklist; fix, rebuild, repeat.

  8. Run the test suite and a production-mode smoke test (next start, then curl or the end-to-end suite), because caching defaults changed silently between 14 and 15: code that relied on a cached fetch is now slower, not broken.

  9. Only then adopt the new features (Cache Components, Partial Prefetching, the React Compiler) one at a time.

The same upgrade can be handed to a coding agent: the version 16 guide ships a prompt that asks the agent to set up version-matched docs first (npx @next/codemod@canary agents-md, or the bundled docs and AGENTS.md block that Next.js 16.2 and 16.3 maintain), run the codemod, explain the plan, and verify the result with the dev tools.

Migrating from Create React App and Vite

Both official guides use the same incremental strategy: first make the existing single-page app run inside Next.js as a client-only application, unchanged, then adopt Next.js features route by route. The motivation the guides give is the usual list (a client-only app downloads all its code before it can fetch data, which causes waterfalls; Next.js adds automatic code splitting, streaming, server-side data fetching, the Proxy and the built-in optimisations). Nothing about the first step needs React Server Components. The two guides are at Create React App and Vite; Create React App is no longer maintained, so that guide is the one most older projects need.

From Create React App

The steps of the guide, applied to a small Create React App style project (a src/App.tsx with state, an image import and a REACT_APP_ variable), were:

  1. Install Next.js (npm install next@latest).

  2. Create next.config.ts with output: 'export' and distDir: 'build'.

  3. Create app/layout.tsx: the <html>, <head> and <body> of public/index.html become the root layout, and the div#root becomes <div id="root">{children}</div>.

  4. Move the title and description into the Metadata API, and drop the charset and viewport tags (Next.js adds them).

  5. Import global CSS from the layout.

  6. Create the entry page as an optional catch-all route, app/[[…​slug]]/page.tsx, with generateStaticParams returning [{ slug: [''] }].

  7. Add a client-only entrypoint, app/[[…​slug]]/client.tsx, that loads the old App with next/dynamic and ssr: false.

  8. Change image imports (they return an object; use logo.src or the Image component).

  9. Rename REACT_APP_ variables to NEXT_PUBLIC_.

  10. Update the package.json scripts and .gitignore, then clean up public/index.html, src/index.tsx, react-scripts and the reportWebVitals setup.

next.config.ts for an SPA migrated from Create React App
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  output: 'export', // Outputs a Single-Page Application (SPA)
  distDir: 'build', // Keeps CRA's build directory name
}

export default nextConfig
app/layout.tsx replacing public/index.html
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'Bookshelf SPA',
  description: 'A Create React App site running on Next.js',
}

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
app/[[…​slug]]/page.tsx and client.tsx: every route served by one client-only page
// app/[[...slug]]/page.tsx
import { ClientOnly } from './client'

export function generateStaticParams() {
  return [{ slug: [''] }]
}

export default function Page() {
  return <ClientOnly />
}

// app/[[...slug]]/client.tsx
'use client'

import dynamic from 'next/dynamic'

const App = dynamic(() => import('../../src/App'), { ssr: false })

export function ClientOnly() {
  return <App />
}
The old App component, with the two edits the guide asks for (logo.src and NEXT_PUBLIC_)
// src/App.tsx
import { useEffect, useState } from 'react'
import logo from './logo.svg'

export default function App() {
  const [clicks, setClicks] = useState(0)
  const [host, setHost] = useState('(server)')
  useEffect(() => setHost(window.location.host), [])
  return (
    <main>
      <img src={logo.src} alt="Bookshelf logo" width={48} height={48} />
      <h1>Bookshelf SPA</h1>
      <p>API: {process.env.NEXT_PUBLIC_API_URL}</p>
      <p>Host: {host}</p>
      <button onClick={() => setClicks(clicks + 1)}>Clicked {clicks} times</button>
    </main>
  )
}

The project was built with npm run build and the build directory served with a static file server on port 3903 (the start script of the guide, npx serve@latest ./build, serves the same directory).

Build output of the migrated SPA, and what the static HTML contains
Route (app)
┌ ○ /_not-found
└   /[[...slug]]
  └ ● /

○  (Static)  prerendered as static content
●  (SSG)     prerendered as static HTML (uses generateStaticParams)

title: ['Bookshelf SPA']
meta description: ['A Create React App site running on Next.js']
has root div: True
has h1 text in the HTML: False

The page title and description are in the HTML (so crawlers and link previews see them), the div#root is there, and the application itself is absent from the HTML because ssr: false makes it client-only, exactly like the original Create React App site. Opened in a browser, the page showed the heading, API: http://localhost:4000/api (the NEXT_PUBLIC_API_URL of .env), the host localhost:3903 filled in by the effect, and the button. From here each route can move into its own page.tsx and out of the client bundle.

The guide’s "Additional Considerations" cover what Create React App configured implicitly: a custom homepage becomes basePath, a service worker is registered with navigator.serviceWorker.register(new URL(…​, import.meta.url)), the proxy field of package.json becomes a rewrites entry in next.config.ts, a custom webpack or Babel configuration needs --webpack (or a Turbopack equivalent), and next-env.d.ts must be in tsconfig.json’s `include. Next.js uses Turbopack by default, whereas Create React App used webpack; next dev --webpack is the closest match.

From Vite

The Vite guide has the same shape (steps 1 to 9) with these differences from the Create React App one:

  • next.config.mjs uses distDir: './dist';

  • tsconfig.json needs nine specific changes (remove the project reference to tsconfig.node.json, add ./dist/types/*/.ts and ./next-env.d.ts to include, the next plugin, esModuleInterop, jsx: react-jsx, allowJs, forceConsistentCasingInFileNames, incremental);

  • the layout replaces index.html, the entry page replaces main.tsx;

  • environment variables change prefix from VITE_ to NEXT_PUBLIC_;

  • Turbopack supports import.meta.env.MODE, DEV, PROD, BASE_URL and SSR without changes (BASE_URL follows basePath and has a trailing slash), but not custom VITE_* variables, Vite modes, envPrefix or envDir;

  • Turbopack supports import.meta.glob (16.3) with no changes, except that the Vite 5 as option (deprecated) is replaced by query, and import.meta.globEager is not supported (use { eager: true }).

The two Vite APIs were exercised in the migrated SPA of the previous section by replacing App.tsx with a component that globs three TypeScript modules and prints import.meta.env.MODE.

import.meta.glob and import.meta.env in a client-only Next.js SPA
// src/App.tsx
const notes = import.meta.glob('./notes/*.ts', { eager: true })

export default function App() {
  return (
    <ul>
      {Object.entries(notes).map(([path, mod]) => (
        <li key={path}>
          {path}: {(mod as { default: string }).default}
        </li>
      ))}
      <li>mode: {import.meta.env.MODE}</li>
    </ul>
  )
}

Built with next build (Turbopack) and opened from the same static server, the page listed ./notes/a.ts: first note, ./notes/b.ts: second note and mode: production. A .txt glob failed with "Unknown module type" because Turbopack needs a registered loader for file types it does not know (the 16.3 post makes the same point for .md); and the eager result is typed Record<string, unknown>, so the cast above is needed.

After either migration the guides' "next steps" are the same: move from React Router to the App Router (see Routing Fundamentals and Client-Side Rendering for the single-page-app mode this leaves you in), then adopt next/image, next/font, next/script and the ESLint config (Images, Fonts & Scripts). The migration that starts from an existing Pages Router application, as opposed to a single-page app, is the incremental Pages to App Router guide, and has its own page in this section.

Removed and Deprecated: A Quick Reference

Table 6. Features that older tutorials still use, with the version that changed them
Feature in old material Version Status and replacement

next export command

14

Removed; output: 'export'.

@next/font

13.2, removed in 14

next/font (built in).

next/image with layout and objectFit

13

Became next/legacy/image, which is deprecated in 16; use next/image and style props.

<Link><a> children

13

Not needed; legacyBehavior is deprecated (and still accepted by 16.3.8).

images.domains

16

Deprecated; use images.remotePatterns.

ImageResponse from next/server

14

next/og.

Synchronous params, searchParams, cookies(), headers(), draftMode()

15 deprecated, 16 removed

Promises; await or React’s use.

experimental-edge runtime value

15

Error; use edge.

ssr: false with next/dynamic in a Server Component

15

Error; move it into a Client Component.

middleware.ts and export function middleware

16

Deprecated; proxy.ts on Node.js. Keep middleware.ts only for the Edge runtime.

revalidateTag(tag) with one argument

16

Deprecated; revalidateTag(tag, 'max') or updateTag(tag) in a Server Action.

unstable_cacheLife, unstable_cacheTag

16

cacheLife, cacheTag.

experimental.ppr, experimental_ppr, experimental.dynamicIO, experimental.useCache

16

Removed; cacheComponents.

experimental.turbopack

16

Top-level turbopack.

next lint, the eslint option of next.config

16

Removed; the ESLint CLI with flat config.

AMP (next/amp, useAmp, config.amp)

16

Removed.

serverRuntimeConfig, publicRuntimeConfig

16

Removed; environment variables.

unstable_rootParams

16

Removed; next/root-params (16.3).

unstable_catchError, unstable_retry

16.3

catchError, retry.

@next/bundle-analyzer (webpack only)

16.1

next experimental-analyze (experimental, Turbopack).

geo and ip on NextRequest

15

Removed; use the hosting provider’s values (for example @vercel/functions).

process.browser

not documented

Listed as removed in older summaries, but neither the 15 nor the 16 guide documents a removal, so no removal version is claimed here. Branch on typeof window, or better, keep browser-only code in a Client Component.

Source: version 14, version 15 and version 16 upgrade guides

The two next/dynamic rows are from the same mechanism: loading a component only in the browser is a Client Component decision. This is the 15 error, seen with a page that does it in a Server Component.

ssr: false in a Server Component, built with Next.js 16.3.8
./app/dyn/page.tsx:3:15
Error: `ssr: false` is not allowed with `next/dynamic` in Server Components. Please move it into a Client Component.
> 3 | const Chart = dynamic(() => import('./chart'), { ssr: false })
The fix: a Client Component wrapper
// app/dyn/chart-client.tsx
'use client'
import dynamic from 'next/dynamic'

export const Chart = dynamic(() => import('./chart'), { ssr: false })

Where the Books Are Outdated

Two books were consulted while preparing this section: Real-World Next.js (Michele Riva, Packt, 2022, Next.js 12.0, React 17, Pages Router only) and Practical Next.js for E-Commerce (Alex Libby, Apress, 2023, Next.js 13.2.4, React 18.2, Pages Router under src/pages; the App Router is explicitly skipped). Both are listed in the section bibliography, and no text, listing or figure of either is reproduced here. The table collects, in one place, every statement of theirs that no longer holds, with the version that changed it and the page of this section that teaches the current approach. The same rows apply to most blog posts of the same years.

Table 7. Outdated in the books, and what is current (Next.js version of the change in brackets)
Area Outdated in the books, and current Covered in

Router and architecture

Pages Router only. The current default is the App Router (stable in 13.4) with Server Components, layouts, loading and error files, Server Actions and Route Handlers. The Pages Router remains supported, but it is legacy for new work.

Introduction & Architecture, Routing Fundamentals

Data functions

getServerSideProps, getStaticProps, getStaticPaths and getInitialProps. The current forms are async Server Components, generateStaticParams, fetch or 'use cache', and revalidatePath, revalidateTag and updateTag.

Data Fetching, Static Site Generation, Server-Side Rendering

Request APIs

Synchronous params, searchParams, cookies() and headers(). They are Promises: asynchronous in 15, and synchronous access removed in 16.

This page, Dynamic Routes & Route Groups

Caching

The 13 and 14 defaults (fetch cached, GET Route Handlers cached, a 30 second router cache). They are uncached by default in 15, with staleTimes.dynamic at 0, and 16 adds Cache Components.

Caching, Partial Prerendering & Cache Components

Middleware

middleware.ts. It is proxy.ts, running on Node.js (16).

Redirects, Rewrites & Proxy

Tooling

A custom .babelrc or webpack setup is replaced by SWC (12) and by Turbopack as the default for dev and build (16). next lint was removed (16), so use the ESLint CLI with the flat config. The next export command was removed (14) in favour of output: 'export'. @next/bundle-analyzer (webpack only) has the Turbopack counterpart next experimental-analyze (16.1, still experimental).

Turbopack & the Compiler, Performance & Web Vitals

Components

<Link passHref><a> and legacyBehavior (deprecated, announced for removal in 16 and still accepted by 16.3.8). next/image with layout and objectFit became next/legacy/image (deprecated), and images.domains became remotePatterns. next/head, the head.js file and next-seo are replaced by the Metadata API (metadata, generateMetadata, file-based metadata).

Linking & Navigation, Images, Fonts & Scripts, Metadata & SEO

Removed features

AMP and serverRuntimeConfig/publicRuntimeConfig (removed in 16); process.browser (no documented removal; see the quick reference above); next/dynamic with ssr: false in Server Components (disallowed in 15).

This page, Configuration, TypeScript & Environment Variables

Runtime and metrics

React 17 and 18 are superseded by React 19 (the Pages Router still accepts 18); Node 14 and 16 (end of life) by Node 20.9 or later; First Input Delay by Interaction to Next Paint (INP), which became a Core Web Vital in 2024.

Performance & Web Vitals

Internationalisation

The i18n key of next.config.js and next-i18next (Pages Router only). The App Router uses app/[locale] segments, locale detection in proxy.ts, and next-intl or i18next directly.

Internationalization

Auth

NextAuth v4 ([…​nextauth].js, NEXTAUTH_* variables) is replaced by Auth.js v5 (the auth() helper, AUTH_* variables, an App Router route handler). The Auth0 SDK v1 (handleAuth(), useUser) is replaced by the v4 Auth0Client with the Proxy. Auth.js is still a beta on npm and the project is now part of Better Auth.

Authentication & Authorization

Ecosystem

Redux createStore became Redux Toolkit; Chakra UI v1 became v3; the Tailwind v2/v3 JavaScript config became Tailwind v4 with CSS-first configuration; GraphCMS became Hygraph; @sanity/client v3 became next-sanity; Apollo SSR through pageProps became @apollo/client-integration-nextjs; fastify-nextjs became @fastify/nextjs; Stripe redirectToCheckout (deprecated) became a redirect to session.url or embedded Checkout; the Netlify runtime became the OpenNext adapter; the Heroku free tier ended in November 2022.

Styling, Route Handlers & Backend for Frontend

Testing

Manual Jest plus Babel is replaced by next/jest (SWC) or Vitest; Playwright is documented alongside Cypress.

Testing

Source: the upgrade guides, INP becomes a Core Web Vital and Heroku: the next chapter; the ecosystem rows are verified on the pages linked in the references

The ecosystem row is the one that changes most quickly, so it carries the date: on 2026-10-03 next-auth was at 4.24.15 with 5.0.0-beta.32 on the beta tag (there is no stable v5 tag), @auth0/nextjs-auth0 at 4.31.0, Vitest at 5.0.3, Playwright at 1.63.0, Tailwind CSS at 4.3.3 and zod at 4.6.5. The books' version assumptions (Vitest 3 or 4 era, NextAuth 4) are older than those.

Errata and Bad Practices Not to Reproduce

The books also contain faults that are not a matter of age. They are collected here with the correct form of each, so that nobody copies them; the pages that teach the topic (data security, authentication, Route Handlers) repeat the relevant one in context. The correct forms below follow the official documentation, and the ones that could be shown as a short program were built and run in the scratch Bookshelf app with next build and next start -p 3903.

Table 8. Errata of the two books, and the correct form
# Fault Correct form

1

Secret exposed to the browser (Libby ch. 3 to 4, 7). A Sanity write token stored as NEXT_PUBLIC_SANITY_TOKEN with the build warning silenced, and the claim that .env keeps it private.

Everything prefixed NEXT_PUBLIC_ is inlined into the client JavaScript, wherever it is defined. Use an un-prefixed variable read in server-only code, rotate any token that was ever shipped, and use a read-only token or none for public reads. See Data Security & CSP.

2

Secret exposed to the browser. A Stripe secret key named NEXT_PUBLIC_STRIPE_SECRET_KEY.

Only the publishable key may use the prefix. The secret key stays in a server-only variable and a server-only module.

3

Price tampering (Libby ch. 4, 7). Checkout line_items are built from the cart posted by the client and its prices; quantities are not validated (missing in both books).

The client sends product ids and quantities only. The server looks the price up by id and validates the quantity. The Route Handler below was run.

4

Payment fulfilment (both books). The success redirect is treated as proof of payment.

The redirect proves nothing. Fulfil an order from the payment provider’s signed webhook, verified on the raw body, answered quickly and idempotent. See Route Handlers & Backend for Frontend.

5

Injection. The slug is interpolated into a GROQ query (Libby ch. 3).

Pass the slug as a query parameter of the CMS client (a GROQ query uses $slug and the client takes the value separately) instead of concatenating it into the query string, and validate it first, as in item 6.

6

Injection and open proxy (Riva ch. 4). An unvalidated path segment is interpolated into an upstream URL, and the API route forwards whatever it is given.

Validate the segment against a strict pattern, build the upstream URL from a fixed host, and look the resource up by id. Run below.

7

Auth (Riva ch. 12, Libby ch. 12). A hard-coded JWT secret and hard-coded credentials.

Secrets come from environment variables (placeholders in the repository, real values in the hosting provider); credentials come from a user store with hashed passwords, or from an identity provider.

8

Auth. No token expiry, and a cookie without secure or sameSite.

Set httpOnly, secure, sameSite and an expiry (maxAge). Run below.

9

Auth. An uncaught jwt.verify error, so a tampered cookie produces a 500.

Treat a failed verification as "not signed in": catch the error and return a 401 or a redirect to sign-in.

10

Auth. Pages protected only by client-side hooks (useAuth, useSession).

A client hook hides UI; it protects nothing. Authorise on the server, in the Server Component, Route Handler or Server Action that reads the data, and use the Proxy only as an optimistic first check. See Authentication & Authorization.

11

Auth. NEXTAUTH_SECRET omitted from the initial setup.

Auth.js v5 needs AUTH_SECRET (and AUTH_TRUST_HOST behind a proxy, or on localhost with next start), and the NextAuth v4 setup of the books needed NEXTAUTH_SECRET. Generate a random value, keep it in the environment, and set it before the first sign-in.

12

Other. useEffect(async () ⇒ …​) with no abort (Riva ch. 4).

An effect callback must be synchronous (it may return only a cleanup function). Put the async work in an inner function and abort the request in the cleanup. Built below.

13

Other. A root-run, single-stage Dockerfile without a .dockerignore that copies .env* (Riva ch. 11).

A multi-stage build, a non-root user, and a .dockerignore that excludes .env*, .git and node_modules. The deployment pages of this section show the Next.js specific parts.

14

Other. req.headers.origin used to build redirect URLs (Libby ch. 4).

The header is controlled by the caller. Build the redirect from a fixed base URL in configuration, a relative path, or an allow-list. Run below.

15

Other. The claims that generateMetadata needs TypeScript and that the App Router is unsuitable (Libby ch. 8).

Neither is true. generateMetadata works in plain JavaScript files (run below), and the App Router is the default for new Next.js applications.

The correct forms that are short enough to show were written as Route Handlers, built, and called with curl.

Errata 3: prices read on the server by id, quantities validated (app/api/quote/route.ts)
import { z } from "zod";
import { getBook } from "@/lib/books";

// The client sends ids and quantities only; prices never come from the request.
const Cart = z.object({
  items: z.array(z.object({ id: z.string(), quantity: z.number().int().min(1).max(10) })).min(1),
});

export async function POST(request: Request) {
  const parsed = Cart.safeParse(await request.json().catch(() => null));
  if (!parsed.success) {
    return Response.json({ error: "invalid cart" }, { status: 400 });
  }
  const lines = [];
  for (const { id, quantity } of parsed.data.items) {
    const book = await getBook(id);
    if (!book) return Response.json({ error: `unknown book ${id}` }, { status: 400 });
    lines.push({ id, quantity, unitCents: book.priceCents });
  }
  const totalCents = lines.reduce((sum, l) => sum + l.unitCents * l.quantity, 0);
  return Response.json({ lines, totalCents });
}
A forged priceCents of 1 is ignored, and a negative quantity is rejected
curl -s -X POST -H 'Content-Type: application/json' \
  -d '{"items":[{"id":"1","quantity":2,"priceCents":1}]}' http://localhost:3903/api/quote
curl -s -w ' [%{http_code}]\n' -X POST -H 'Content-Type: application/json' \
  -d '{"items":[{"id":"1","quantity":-5}]}' http://localhost:3903/api/quote
Result of the quote Route Handler for the two requests
{"lines":[{"id":"1","quantity":2,"unitCents":999}],"totalCents":1998}
{"error":"invalid cart"} [400]

Source: Data security and route.js

Errata 6: a validated segment and a lookup by id (app/api/upstream/[id]/route.ts)
import { getBook } from "@/lib/books";

// Validate the segment against a strict pattern and look it up by id,
// instead of interpolating it into an upstream URL.
export async function GET(_req: Request, ctx: RouteContext<"/api/upstream/[id]">) {
  const { id } = await ctx.params;
  if (!/^\d{1,6}$/.test(id)) {
    return Response.json({ error: "bad id" }, { status: 400 });
  }
  const book = await getBook(id);
  return book ? Response.json(book) : Response.json({ error: "not found" }, { status: 404 });
}
Results of the validated upstream Route Handler
$ curl -s -w ' [%{http_code}]\n' http://localhost:3903/api/upstream/2
{"id":"2","title":"Neuromancer","author":"William Gibson","priceCents":899} [200]
$ curl -s -w ' [%{http_code}]\n' 'http://localhost:3903/api/upstream/..%2Fadmin'
{"error":"bad id"} [400]
Errata 8: a cookie with the flags and an expiry (app/api/demo-cookie/route.ts)
import { cookies } from "next/headers";

export async function POST() {
  const jar = await cookies();
  jar.set("session", crypto.randomUUID(), {
    httpOnly: true,
    secure: true,
    sameSite: "lax",
    path: "/",
    maxAge: 60 * 60, // expires after one hour
  });
  return Response.json({ ok: true });
}
The Set-Cookie header of the demo cookie
$ curl -si -X POST http://localhost:3903/api/demo-cookie | grep -i set-cookie
set-cookie: session=<random uuid>; Path=/; Expires=Sat, 03 Oct 2026 18:05:39 GMT; Max-Age=3600; Secure; HttpOnly; SameSite=lax
Errata 14: the redirect target comes from configuration, not from a header (app/api/login-redirect/route.ts)
import { NextResponse } from "next/server";

// The redirect target is built from configuration, never from a request header.
const SITE_URL = process.env.SITE_URL ?? "http://localhost:3903";

export async function POST() {
  return NextResponse.redirect(new URL("/en/dashboard", SITE_URL), 303);
}
The redirect target ignores the forged Origin header
$ curl -si -X POST -H 'Origin: https://evil.example' http://localhost:3903/api/login-redirect | grep -i -E '^(HTTP|location)'
HTTP/1.1 303 See Other
location: http://localhost:3903/en/dashboard
Errata 12: a synchronous effect callback with an inner async function and an abort (app/[locale]/abort-effect/book-count.tsx)
"use client";

import { useEffect, useState } from "react";

export function BookCount() {
  const [count, setCount] = useState<number | null>(null);

  useEffect(() => {
    const controller = new AbortController();
    // The effect callback itself stays synchronous; the async work lives inside it.
    async function load() {
      try {
        const res = await fetch("/api/books", { signal: controller.signal });
        setCount((await res.json()).length);
      } catch (error) {
        if ((error as Error).name !== "AbortError") throw error;
      }
    }
    load();
    return () => controller.abort();
  }, []);

  return <p>{count === null ? "Loading…" : `${count} books`}</p>;
}

The component was compiled and type-checked by next build, and the server-rendered HTML contained Loading…; the effect itself runs only in the browser. The React side of the rule is on the Effects page.

Errata 15: generateMetadata in a plain JavaScript file (app/[locale]/meta-js/page.jsx)
export async function generateMetadata({ params }) {
  const { locale } = await params
  return { title: `Plain JavaScript metadata (${locale})` }
}

export default function Page() {
  return <p>generateMetadata works without TypeScript.</p>
}
The title rendered from the plain JavaScript generateMetadata
$ curl -s http://localhost:3903/en/meta-js | grep -o '<title>[^<]*</title>'
<title>Plain JavaScript metadata (en)</title>

The remaining errata (1, 2, 4, 5, 7, 9, 10, 11 and 13) are stated as rules with the page that shows the full example in context, because the correct form is a convention (where a variable lives, how a webhook is verified) rather than a program that could be shown here with a result. Items 1, 2, 7 and 11 are the same lesson: a secret belongs to the server, and the repository holds only placeholders.

New Since the Books

The books do not have these. Each is detailed on the page of the section that covers it.

  • App Router: layouts and templates, route groups, parallel and intercepting routes, loading, error, not-found, forbidden and unauthorized, Route Handlers, the Metadata API, next/form, typed routes and the PageProps and LayoutProps helpers, and next/root-params (16.3). See Layouts, Templates & Special Files.

  • React Server Components and the RSC payload, 'use client' and 'use server', Server Actions with useActionState and useOptimistic, after, connection, and catchError. See Server & Client Components and Server Actions & Mutations.

  • Rendering and caching: streaming with Suspense, Partial Prerendering and Cache Components ('use cache', 'use cache: private', 'use cache: remote', cacheLife, cacheTag, updateTag, refresh), and ISR with an app shell for unknown params (16.3). See Streaming & Suspense and Incremental Static Regeneration.

  • Navigation: the 16.0 prefetch rewrite (layout deduplication, incremental prefetching), Instant Navigations and Partial Prefetching (16.3), <Activity>-preserved routes, View Transitions and useLinkStatus.

  • Tooling: Turbopack (default), the React Compiler, the hydration diff indicator (16.2), the Next.js DevTools MCP, AGENTS.md, next upgrade, next typegen, the stable Adapters API (16.2), instrumentation-client and @next/playwright. See Hydration and Debugging, Instrumentation & Observability.

Topics the books do not cover at all, which the official documentation supplies and the section teaches: data security (the Data Access Layer, server-only, the taint APIs, Server Action closure encryption and allowed origins), Content Security Policy with nonces, draft mode, MDX, multi-zones, multi-tenant apps, PWAs and offline support, OpenTelemetry and instrumentation, self-hosting (cache handlers, version skew, deploymentId), CDN caching, memory usage, the Edge runtime, the custom-server caveats, single-page-app mode and the pattern for preventing a flash before hydration. The first of these is on Data Security & CSP.

Upgrade Pitfalls

Table 9. Symptoms after an upgrade, and the usual cause
Symptom Usual cause

Property 'get' does not exist on type 'Promise<ReadonlyRequestCookies>'

A synchronous cookies() or headers() call from before 15. Run next-async-request-api.

Expected 2 arguments, but got 1 on revalidateTag

The 16 signature; add a profile such as 'max', or use updateTag in a Server Action.

next build fails with "This build is using Turbopack, with a webpack config"

A webpack function, often added by a plugin. Add turbopack: {}, migrate the config, or pass --webpack.

Unrecognized key(s) in object: 'turbopack' at "experimental"

The key moved to the top level in 16.

Route segment config "instant" requires nextConfig.cacheComponents

The cache-components-instant-false codemod ran while cacheComponents is off.

Module not found: Can’t resolve 'next/amp'

AMP was removed in 16.

ssr: false is not allowed with next/dynamic in Server Components

Move the dynamic() call into a Client Component.

A parallel route slot returns 404 on a hard refresh

The slot has no default.js (required in 16).

next lint prints "Invalid project directory …​ lint"

The command was removed in 16; use eslint . (codemod next-lint-to-eslint-cli).

Slower pages after upgrading from 14 to 15

fetch and GET handlers are no longer cached; add cache: 'force-cache', dynamic = 'force-static' or use cache.

More, smaller prefetch requests after upgrading to 16

The routing and prefetching rewrite (layout deduplication, incremental prefetching), expected by design.

Source: invalid-next-config and the upgrade guides in the references

Summary

  • The big changes by version: 13 (the App Router), 14 (Server Actions stable, next export removed), 15 (async request APIs, uncached by default, React 19), 16 (Turbopack default, proxy.ts, Cache Components, synchronous APIs and several features removed) and 16.1 to 16.3 (stable dev cache, Adapters, catchError, next/root-params, Instant Navigations opt-in).

  • npx next upgrade (16.1 and later) or npx @next/codemod@canary upgrade latest upgrades the packages and runs the recommended codemods, but not all of them: run next-async-request-api by hand, fix next.config and revalidateTag manually, and accept cache-components-instant-false only when adopting Cache Components.

  • Create React App and Vite apps move to Next.js as a client-only catch-all page with output: 'export', and then adopt features route by route.

  • cacheComponents and partialPrefetching remain opt-in in 16.3; the section teaches both caching models.

  • Thirteen areas of the books are outdated and fifteen items are errata; the tables above list each with the correct form and the page that teaches it.