The temporary Next.js 15 compatibility path is gone.
Beforetypescript
// Next.js 15 temporary synchronous compatibility
import { cookies, type UnsafeUnwrappedCookies } from 'next/headers';
export function readTheme() {
const store = cookies() as unknown as UnsafeUnwrappedCookies;
return store.get('theme')?.value;
}
Aftertypescript
// Next.js 16: remove synchronous access
import { cookies } from 'next/headers';
export async function readTheme() {
const store = await cookies();
return store.get('theme')?.value;
}
Migration
Await cookies, headers, draftMode, params and searchParams throughout App Router code.
The optimizer rejects private/local IP destinations by default.
Beforetsx
import Image from 'next/image';
// Private-IP source that may now be rejected.
export default function Photo() {
return <Image src='http://10.0.0.5/photo.jpg' width={640} height={480} alt='Catalog' />;
}
Aftertsx
import Image from 'next/image';
// Prefer a trusted public origin, allowlisted with images.remotePatterns.
export default function Photo() {
return <Image src='https://assets.example.com/photo.jpg' width={640} height={480} alt='Catalog' />;
}
// Do not enable dangerouslyAllowLocalIP as a blanket migration fix.
Migration
Prefer a trusted public image host. Enable dangerouslyAllowLocalIP only for a controlled private network after assessing SSRF exposure.
// pages/article.tsx: standard page, no AMP APIs/configuration
export default function Article() {
return <main><h1>Article</h1><p>Web edition</p></main>;
}
// Replace AMP-specific elements and review former AMP URLs/canonicals.
Migration
Remove AMP APIs and rework AMP-only markup as standard pages; review canonical URLs and redirects.
Missing default.js/default.tsx files in parallel slots can fail the build.
Beforetsx
// app/@modal/page.tsx exists, but @modal has no default.tsx.
export default function ModalPage() {
return null;
}
Aftertsx
// Add app/@modal/default.tsx for unmatched direct navigation.
export default function ModalDefault() {
return null;
}
// Add an appropriate default for each parallel slot;
// use notFound() if unmatched routes should return 404.
Migration
Add a fallback for every slot, returning null or calling notFound() as appropriate; test direct navigation and reloads.
experimental.ppr, dynamicIO, useCache and route experimental_ppr require review; Cache Components uses a different rendering model.
Beforejavascript
// Experimental PPR on Next.js 15 canaries
export default { experimental: { ppr: true } };
// Routes may also export experimental_ppr.
Afterjavascript
// Only when intentionally migrating to Cache Components:
export default { cacheComponents: true };
// Remove experimental_ppr and migrate affected routes.
// Review Suspense boundaries and caching; this is not just a flag rename.
// If not adopting this model, remove old flags instead of enabling it.
Migration
Remove unused experimental flags. For active PPR adoption, follow the Cache Components migration with Suspense and cache boundaries; do not treat cacheComponents as a simple flag rename.
serverRuntimeConfig, publicRuntimeConfig and next/config consumers need migration.
Beforetypescript
import 'server-only';
import getConfig from 'next/config';
export function readApiToken() {
return getConfig().serverRuntimeConfig.apiToken;
}
Aftertypescript
import 'server-only';
import { connection } from 'next/server';
export async function readApiToken() {
await connection(); // read this value at request time
return process.env.API_TOKEN;
}
// Never prefix secrets with NEXT_PUBLIC_.
Migration
Use server-only environment variables for secrets. Use NEXT_PUBLIC_ only for public build-time values; use connection() when server values must be evaluated at request time.
// next.config.mjs: optional, not required for the upgrade
export default { reactCompiler: true };
// Keep babel-plugin-react-compiler installed if opting in.
Migration
If already using the compiler, move the option out of experimental and keep babel-plugin-react-compiler installed. Otherwise no enablement is required.
// Only for public data intended to be statically cached.
export const dynamic = 'force-static';
export async function GET() {
return Response.json({ categories: ['books', 'music'] });
}
Migration
Opt eligible handlers into static caching with export const dynamic = 'force-static'.
// middleware.ts
import { NextResponse, type NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname === '/old-guide') {
return NextResponse.redirect(new URL('/guides', request.url));
}
return NextResponse.next();
}
Aftertypescript
// proxy.ts: verify Node.js runtime compatibility
import { NextResponse, type NextRequest } from 'next/server';
export function proxy(request: NextRequest) {
if (request.nextUrl.pathname === '/old-guide') {
return NextResponse.redirect(new URL('/guides', request.url));
}
return NextResponse.next();
}
Migration
For Node.js-compatible middleware, run npx @next/codemod@latest middleware-to-proxy .; proxy does not support Edge runtime. Keep middleware if Edge is required.