Upgrading Next.js safely
Major version upgrades in Next.js aren't just about picking up new features—they often ship breaking changes that silently break production if you're not careful. I've seen teams upgrade from 13 to 14 or 14 to 15 without reading the release notes, only to discover caching behavior changed, API routes behave differently, or middleware patterns no longer work. Upgrading Next.js safely means understanding what changed before you run npm install, testing against production builds, and validating your entire application flow before deploying.
Read the upgrade guide and codemods
Before touching your package.json, spend 20 minutes reading the official upgrade guide. Next.js maintains detailed migration docs for each major version bump. They'll tell you about deprecations, breaking changes, and new required configs that didn't exist in your current version.
Start here: open the Next.js GitHub releases page or the docs site and search for "upgrade from X to Y." You'll find a structured list of what's deprecated, what's removed, and what requires manual action. Don't skim it—read the sections that apply to your codebase.
The second step is running codemods. Next.js ships automated scripts that rewrite your code to match new APIs. For example, when Image component props changed between versions, the codemod rewrote every <Image> call automatically.
Here's how to run them:
npx @next/codemod@latest [codemod-name] ./src
Common codemods include:
next-image-to-legacy-image– converts new Image syntax back to legacy (if downgrading)next-image-experimental– updates Image for new optimization propscra-to-next– helps migrate from Create React Appadd-missing-react-fc-return-type– fixes TypeScript issues with React components
After running codemods, review the changes in git diff. Codemods are smart but not perfect—you might need to tweak a few files manually. This is normal and expected.
Check caching and request API changes
Caching behavior is one of the most dangerous areas in Next.js upgrades because it's invisible until production breaks. Between versions, defaults shift, and routes that used to revalidate on demand might now cache indefinitely, or vice versa.
For example, in Next.js 13 App Router, the default for fetch() requests was cache: 'force-cache'. This means data fetched during build or first request stayed cached forever. In later versions, the defaults changed, and some routes started revalidating more aggressively.
Before upgrading, audit your data fetching patterns:
- Search your codebase for
fetch()calls without explicit cache options - Check for routes using
revalidateorISR(Incremental Static Regeneration) - Review middleware that depends on request/response lifecycle
- Identify API routes that handle mutations or depend on request context
Make caching explicit. Instead of relying on defaults, specify exactly what you want:
// Explicit cache—won't change across versions
export const fetchPostById = async (id: string) => {
const res = await fetch(`https://api.example.com/posts/${id}`, {
cache: 'force-cache', // or 'no-store', or 'revalidate: 3600'
next: { tags: ['posts'] }
});
return res.json();
};
// With revalidation
export const fetchUserProfile = async (userId: string) => {
const res = await fetch(`https://api.example.com/users/${userId}`, {
next: { revalidate: 300 } // revalidate every 5 minutes
});
return res.json();
};
If you're using the Pages Router with getStaticProps or getServerSideProps, the upgrade guide will tell you if those functions have new signatures or behavior. Check if your revalidate times, fallback strategies, or incremental regeneration need updates.
Request and response APIs also evolve. Middleware, API routes, and server actions sometimes get new context objects or deprecated methods. For instance, accessing request headers might require a different import or API between versions. Read the specific sections of the upgrade guide that mention "requests," "responses," or "middleware."
Upgrade React and Next.js together
Never upgrade Next.js without upgrading React. Next.js is tightly coupled to React's version, and mismatches cause subtle bugs, missing hooks, and broken features.
Check the Next.js release notes to find the required React version. For example, Next.js 15 requires React 18.3 or higher. If you're on React 17, you can't just upgrade Next.js and skip React—you'll hit TypeScript errors, missing hook implementations, or broken server components.
Here's the safe order:
- Read the upgrade guide for both Next.js and React
- Identify the minimum React version required by your target Next.js version
- Upgrade React first, test in dev mode
- Then upgrade Next.js
- Run codemods for both packages
- Fix any remaining type errors or deprecated usage
Example package.json update:
{
"dependencies": {
"react": "^18.3.0",
"react-dom": "^18.3.0",
"next": "^15.0.0"
}
}
After updating, run npm install and then npm run dev. You'll immediately see if there are TypeScript errors or missing dependencies. Fix those before moving forward.
Test with production builds, not dev
Development mode hides problems. Next.js dev server has different caching, tree-shaking, and optimization behavior than production. You can upgrade, run npm run dev, everything looks fine—then deploy to production and watch requests fail.
Always test with production builds before deploying:
npm run build
npm run start
The build command catches static generation errors, missing environment variables, and build-time issues. The start command runs the production server with real caching and optimization applied.
Walk through your critical user journeys in this production build:
- Sign up and log in
- View protected pages
- Fetch data that uses different cache strategies
- Test API endpoints that handle mutations
- Verify middleware runs correctly
- Check that images load and are optimized
- Test dynamic routes and catch-all segments
If your app pulls environment variables, make sure they're available during build. If you use feature flags, test both enabled and disabled states. If you have database calls in getStaticProps or server components, make sure the database is accessible during build and at runtime.
I recommend testing production builds in a staging environment that mirrors production as closely as possible. Use the same database, environment variables, and external APIs. Run automated tests and manual smoke tests against this staging build before merging to main.
Common mistakes
- Skipping the upgrade guide: "I'll just run npm install and see what breaks" wastes hours. Read the guide once, and you'll know exactly what to expect.
- Assuming dev mode is enough: Dev mode doesn't apply the same optimizations or caching. A build that works in dev might fail in production.
- Mixing React and Next.js versions: Upgrading only one causes cryptic errors. Always upgrade both together.
- Relying on implicit caching defaults: Don't assume fetch defaults are the same as they were. Specify cache behavior explicitly in every critical route.
- Forgetting to test middleware and API routes: These are easy to overlook during upgrades, but request/response APIs often change, and bugs here break production silently.
- Not running codemods: They're optional but save hours. Running them catches API changes you might miss manually.
Key takeaways
- Read the official upgrade guide before any code changes—breaking changes are documented, and you'll save time by knowing what to expect.
- Run codemods to automatically update your codebase to match new APIs, then review and fix edge cases manually.
- Audit your caching strategy: make fetch cache behavior explicit, test revalidation logic, and verify middleware request/response handling.
- Upgrade React and Next.js together—never skip one or the other.
- Test production builds in staging, not just dev mode—caching, optimization, and build-time issues only surface in production builds.
- Walk through critical user journeys (auth, data fetching, API calls, protected routes) in your staging production build before merging to main.






Comments
Loading comments…