next-upgrade
Next.js Upgrade Workflow
Structured 9-step workflow for upgrading Next.js applications across major versions. Handles codemod automation, dependency updates, breaking change resolution, and validation.
When to Apply
Use this skill when:
- Upgrading Next.js to a new major version (13, 14, 15, 16)
- Running codemods to automate breaking change migrations
- Resolving deprecation warnings in an existing Next.js project
- Planning an incremental migration path for large codebases
- Validating that an upgrade did not introduce regressions
9-Step Upgrade Workflow
Step 1: Detect Current Version
Identify the current Next.js version and target version.
# Check current version
cat package.json | grep '"next"'
# Check Node.js version (Next.js 15+ requires Node 18.18+, Next.js 16 requires Node 20+)
node --version
Version Requirements:
| Next.js | Minimum Node.js | Minimum React |
|---|---|---|
| 13 | 16.14 | 18.2.0 |
| 14 | 18.17 | 18.2.0 |
| 15 | 18.18 | 19.0.0 |
| 16 | 20.0 | 19.0.0 |
Step 2: Create Upgrade Branch
git checkout -b upgrade/nextjs-{target-version}
Always upgrade on a dedicated branch. Never upgrade on main directly.
Step 3: Run Codemods
Use the official Next.js codemod CLI to automate breaking change migrations.
# Interactive mode (recommended) -- selects applicable codemods
npx @next/codemod@latest upgrade latest
# Or target a specific version
npx @next/codemod@latest upgrade 15
npx @next/codemod@latest upgrade 16
Key Codemods by Version:
Next.js 13 to 14
next-image-to-legacy-image-- Renamesnext/imageimports tonext/legacy/imagenext-image-experimental-- Migrates fromnext/legacy/imageto newnext/imagemetadata-- Moves Head metadata to Metadata API exports
Next.js 14 to 15
next-async-request-apis-- Converts synchronous dynamic APIs (cookies(),headers(),params,searchParams) to asyncnext-dynamic-ssr-false-- Replacesssr: falsewith{ loading }pattern fornext/dynamicnext-og-import-- Moves OG image generation imports tonext/og
Next.js 15 to 16
next-use-cache-- Convertsunstable_cacheto'use cache'directivenext-cache-life-- Migrates cache revalidation tocacheLife()APInext-form-- Wraps<form>elements withnext/formwhere applicable
Step 4: Update Dependencies
# Update Next.js and React together
npm install next@latest react@latest react-dom@latest
# For Next.js 15+, also update React types
npm install -D @types/react@latest @types/react-dom@latest
# Update eslint config
npm install -D eslint-config-next@latest
Peer Dependency Conflicts:
If you encounter peer dependency conflicts:
- Check which packages require older React/Next versions
- Update those packages first, or check for newer versions
- Use
--legacy-peer-depsonly as a last resort (document why)
Step 5: Update Configuration
Review and update next.config.js / next.config.ts:
// next.config.ts (Next.js 15+ recommends TypeScript config)
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
// Next.js 15+: experimental features that graduated
// Remove these from experimental:
// - serverActions (now stable in 14+)
// - appDir (now stable in 14+)
// - ppr (now stable in 16+)
// Next.js 16+: new cache configuration
cacheComponents: true, // Enable component-level caching
};
export default nextConfig;
Configuration Changes by Version:
| Version | Change |
|---|---|
| 14 | appDir removed from experimental (now default) |
| 14 | serverActions removed from experimental (now stable) |
| 15 | bundlePagesRouterDependencies now default true |
| 15 | swcMinify removed (now always enabled) |
| 16 | dynamicIO replaces several caching behaviors |
| 16 | cacheComponents: true enables component caching |
Step 6: Resolve Breaking Changes
After running codemods, manually resolve remaining breaking changes.
Common Breaking Changes (15 to 16):
-
Async Request APIs:
cookies(),headers(),params,searchParamsare now async// Before (Next.js 14) export default function Page({ params }: { params: { id: string } }) { const { id } = params; } // After (Next.js 15+) export default async function Page({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; } -
Caching Default Changed:
fetch()requests are no longer cached by default in Next.js 15+// Before: cached by default fetch('https://api.example.com/data'); // After: explicitly opt-in to caching fetch('https://api.example.com/data', { cache: 'force-cache' }); // Or use 'use cache' directive in Next.js 16 -
Route Handlers: GET route handlers are no longer cached by default
// Next.js 15+: explicitly set caching export const dynamic = 'force-static';
Step 7: Run Tests
# Run existing test suite
npm test
# Run build to catch compile-time errors
npm run build
# Run dev server and check key pages manually
npm run dev
Validation Checklist:
- Build completes without errors
- All existing tests pass
- Key user flows work in dev mode
- No console warnings about deprecated APIs
- Server-side rendering works correctly
- Client-side navigation works correctly
- API routes return expected responses
- Middleware functions correctly
- Static generation (SSG) pages build correctly
Step 8: Update TypeScript Types
# Regenerate TypeScript declarations
npm run build
# Fix any new type errors
npx tsc --noEmit
Common Type Fixes:
PagePropstype changes (params/searchParams become Promise in 15+)Metadatatype updates (new fields added)NextRequest/NextResponseAPI changes- Route handler parameter types
Step 9: Document and Commit
# Create detailed commit
git add -A
git commit -m "chore: upgrade Next.js from {old} to {new}
Breaking changes resolved:
- [list specific changes]
Codemods applied:
- [list codemods run]
Manual fixes:
- [list manual changes]"
Incremental Upgrade Path
For large version jumps (e.g., 13 to 16), upgrade incrementally:
Next.js 13 -> 14 -> 15 -> 16
Why incremental?
- Codemods are version-specific and may not compose correctly across multiple versions
- Easier to debug issues when changes are smaller
- Each version has its own set of breaking changes to resolve
- Tests can validate each intermediate step
For each version step:
- Run codemods for that version
- Update deps
- Fix breaking changes
- Run tests
- Commit checkpoint
- Proceed to next version
Troubleshooting
Build fails after upgrade
- Clear
.nextdirectory:rm -rf .next - Clear node_modules:
rm -rf node_modules && npm install - Clear Next.js cache:
rm -rf .next/cache
Module not found errors
- Check if package was renamed or merged
- Update imports per migration guide
- Check if package needs separate update
Hydration mismatches after upgrade
- Check for server/client rendering differences
- Ensure dynamic imports use correct options
- Verify date/locale handling is consistent
Middleware issues
- Middleware API changed in Next.js 13 (moved to root)
NextResponse.rewrite()behavior changed in 15- Check matcher configuration syntax
Iron Laws
- ALWAYS upgrade on a dedicated branch, never on main directly — upgrade branches can be rebased or reverted without disrupting production; direct main upgrades risk deploying half-migrated code.
- NEVER skip intermediate versions in a multi-version jump — Next.js codemods are version-specific and do not compose correctly across major versions; skipping steps leaves un-migrated breaking changes.
- ALWAYS run official codemods before making manual changes — codemods handle the bulk of mechanical migrations; manual-first approaches miss patterns and create divergence from the reference migration path.
- NEVER use
--legacy-peer-depswithout documenting the specific conflict and resolution plan — suppressing peer errors hides version conflicts that will cause runtime failures. - ALWAYS validate with a full build plus test suite before merging — the dev server does not exercise SSG, edge runtime, or build optimizations that can fail silently post-upgrade.
Anti-Patterns
| Anti-Pattern | Why It Fails | Correct Approach |
|---|---|---|
| Upgrading on the main branch directly | Half-migrated code can reach production; rollback requires a revert commit | Always create upgrade/nextjs-{version} branch; merge only after full validation |
| Skipping intermediate versions | Version-specific codemods are not composable; skipped breaking changes cause runtime failures | Upgrade one major version at a time: 13→14→15→16; commit a checkpoint at each step |
| Manual migration before running codemods | Creates divergence from codemod output; codemods cannot merge cleanly with manual edits | Run codemods first; apply manual fixes only for patterns codemods could not handle |
Using --legacy-peer-deps without documentation |
Hidden version conflicts cause runtime failures not visible at install time | Resolve conflicts explicitly; use the flag only with a documented justification |
| Validating only in dev mode | Dev server skips SSG, edge runtime, and build optimizations that can fail post-upgrade | Run npm run build plus the full test suite; check SSR, SSG, and API routes explicitly |