Next.js 15 is the version where the App Router stopped feeling experimental and started feeling like the right default. The changes are real, several of them are breaking, and most of the confusion I've seen online comes from people upgrading without understanding the philosophy shift underneath the API changes. Here's what I'd want to read before touching a Next.js 14 codebase.
The biggest mental shift: caching is opt-in now
In Next.js 14, fetch() inside Server Components was cached by default. If you fetched the same URL twice across different requests, the second one hit a cache. This was aggressive and surprising — developers kept getting stale data and couldn't figure out why.
Next.js 15 flipped this: fetch() is uncached by default. Every call fetches fresh data unless you explicitly opt into caching.
// Next.js 15 — uncached by default
const data = await fetch('https://api.example.com/data');
// Opt into caching explicitly
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 } // cache for 1 hour
});
// Force cache (static)
const data = await fetch('https://api.example.com/data', {
cache: 'force-cache'
});If your app relied on the old default caching behaviour, things will change on upgrade. The flip side: the new defaults are far more predictable for data that should be fresh.
cookies(), headers(), and params are now async
This is the breaking change that will touch the most code. Dynamic APIs that read from the request — cookies(), headers(), searchParams, and route params — are now async and must be awaited.
// Next.js 14 (synchronous)
import { cookies } from 'next/headers';
export default function Page() {
const token = cookies().get('token');
// ...
}
// Next.js 15 (async)
import { cookies } from 'next/headers';
export default async function Page() {
const cookieStore = await cookies();
const token = cookieStore.get('token');
// ...
}Same for headers():
const headersList = await headers();
const userAgent = headersList.get('user-agent');And for route params in page and layout components:
// Next.js 15
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
// ...
}The @next/codemod transform handles most of this automatically: npx @next/codemod@canary upgrade latest. Run it, review the diff, and fix any edge cases by hand. The codemod is good but not perfect — check component props and middleware especially.
Turbopack is now the default in development
next dev now uses Turbopack by default. For most projects this just means faster local builds and HMR — Turbopack is substantially faster than webpack for incremental compilation. The practical impact: cold starts and hot reloads that used to take 2–4 seconds on a large app now take under a second.
If you hit a Turbopack incompatibility (custom webpack plugins that don't have Turbopack equivalents are the most common issue), fall back with next dev --turbopack false. The webpack build is still there; Turbopack didn't replace it for production.
React 19 and the new hook surface
Next.js 15 ships with React 19 support. The hook additions most relevant to App Router work:
use() — reads from a Promise or Context. Suspends if the Promise isn't resolved, resumes when it is. Useful for deferring non-critical data so the page renders before everything is ready:
import { use, Suspense } from 'react';
function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
const comments = use(commentsPromise);
return <ul>{comments.map(c => <li key={c.id}>{c.body}</li>)}</ul>;
}
// In the Server Component:
const commentsPromise = fetchComments(postId); // not awaited
return (
<Suspense fallback={<p>Loading comments…</p>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
);useActionState() — replaces the old useFormState from react-dom. Manages the state returned by a Server Action form submission.
useOptimistic() — not new in 15 but now stable. Apply optimistic state on the client while a Server Action is in flight.
Partial Pre-rendering (PPR) — still experimental, but important to understand
PPR is the architectural feature that makes Next.js 15 interesting for the long term. The idea: a page can have a static outer shell (rendered at build time) with dynamic holes filled at request time. The static parts are served instantly from the CDN; the dynamic parts stream in.
You opt in per-route today:
// app/dashboard/page.tsx
export const experimental_ppr = true;It's still experimental (experimental_ppr), and I wouldn't ship it to production without testing thoroughly. But understanding what it enables is worth your time: it's the end state of the static/dynamic split that the App Router has been working toward.
What to do on an existing Next.js 14 project
- Run
npx @next/codemod@canary upgrade latest— handles the async API migrations. - Audit your
fetch()calls — if you relied on default caching, decide which calls genuinely need caching and addnext: { revalidate }explicitly. - Test Turbopack locally — run
next devand watch for any custom webpack plugin errors. - Update your type annotations —
paramsandsearchParamsare nowPromise<{...}>, not{...}. - Check middleware —
cookies()andheaders()in middleware need the async treatment too.
The upgrade is worth doing. The caching model is better, the async APIs are more honest about what they're doing, and Turbopack makes the dev loop noticeably faster. Plan for half a day on a medium-sized app.
FAQ
Will my Next.js 14 app break immediately when I upgrade to 15?
The async request APIs (cookies, headers, params) will generate warnings in development and will throw in production if not awaited. Run the codemod first, then manually check the output.
Should I use Turbopack in production? Not yet — production still uses webpack. Turbopack is the dev server only in Next.js 15.
Is the new caching default better?
Yes, for almost every app. The old default-cached fetch() caused silent staleness bugs that were hard to debug. The new opt-in model is more explicit. If you had performance tuned around the old defaults, you'll need to add explicit revalidation intervals.
What happened to getServerSideProps and getStaticProps?
They still work in the Pages Router. They don't exist in the App Router — you use Server Components and fetch() instead. Next.js 15 doesn't remove the Pages Router.