Overview
This document captures the complete upgrade of a Frontend App from Next.js 12 to Next.js 16, including all breaking changes encountered, migration strategies employed, and lessons learned during the process.
Architecture Decision Record (ADR)
Status: Accepted and Completed
Context
We needed to upgrade our Frontend App from Next.js 12 to Next.js 16. This upgrade involved:
Migration from Pages Router to App Router
React 18 to React 19 upgrade
Node.js 18 to Node.js 20.9.0+ requirement
Multiple breaking changes in core packages (MUI, routing, middleware)
multiple package dependency updates
Two approaches were considered:
1. Version-by-version upgrade
Upgrade Next.js incrementally (12→13→14→15→16), fixing breaking changes at each step.
2. Module-by-module upgrade
Jump directly to target version, then fix broken modules and breaking changes one by one
Decision
We chose the module-by-module approach - upgrading directly to Next.js 16 and fixing each broken module systematically.
Why we rejected version-by-version:
Time constraints made incremental upgrades impractical
Each version step would require full regression testing
Many breaking changes compound across versions anyway
Would require maintaining intermediate states
Why module-by-module worked:
POC-first validation: Fixed one page (login) completely before proceeding, proving the approach viable.
Skeleton structure creation: Used Cursor Plan Mode to generate App Router structure, accepting a "broken but structured" starting point
Import aliases preserved stability: Configured @src/, @config/ aliases in tsconfig.json and next.config.js so directory restructuring didn't break imports
Grouped similar fixes: Batched TypeScript errors and package migrations by pattern (e.g., all toaster.notify → ShowErrorToaster replacements done together)
Consequences
Positive:
Faster overall migration (no intermediate version)
Clear progress tracking (module-by-module checklist)
Easier to parallelize work across team members by distributing specific modules to my peers.
Single target state to test against, No "works in v14 but breaks in v15" surprises.
Negative:
The initial state was completely broken.
Required comprehensive planning upfront.
Harder to isolate root cause when multiple things broke simultaneously
Mitigations applied:
Used overrides in package.json for peer dependency conflicts.
Leveraged Cursor for bulk pattern replacements.
3. Pre-Migration Planning
1. Create Comprehensive Upgrade Plan
Created a comprehensive upgrade plan in Cursor using Plan Mode
Provided full repository context and included key files:
package.json
next.config.js
_app.tsx (Pages Router)
_document.tsx
Used Bombay Softwares Next.js boilerplate as reference for App Router structure
2. Configure package.json for Compatibility
Added the overrides section in package.json to handle React 19, Next.js 16, and Node 22 compatibility, This prevents errors during npm install.
{
"engines" : {
"npm" : ">=6.0.0" ,
"node" : ">=20.9.0"
},
"overrides" : {
"react-html-parser" : {
"react" : "^19.0.0" ,
"react-dom" : "^19.0.0"
},
"@recogito/annotorious-openseadragon" : {
"openseadragon" : "^5.0.1"
}
}
}
3. Execute Plan - Create Skeleton Structure
Executed the plan in Cursor
This allowed creation of a skeleton structure of the newer version of Next.js
Expected outcome: A broken application where issues can be fixed one by one
4. POC
For proof-of-concept, completely fixed only 1 page (login page) of the newer version
This validated that the upgrade was possible before proceeding with full migration
Once login page worked end-to-end, we proceeded with remaining pages
4. Directory Structure Migration
1. Import Aliases Configuration
Added aliases for imports which resolved import issues when directory structure changed
This approach didn't break any imports during the restructuring
tsconfig.json:
{
"compilerOptions" : {
"baseUrl" : "." ,
"paths" : {
"@src/*" : ["src/*" ],
"@public/*" : ["public/*" ],
"@interfaces/*" : ["src/interfaces/*" ],
"@config" : ["src/config" ],
"@config/*" : ["src/config/*" ]
}
}
}
next.config.js:
webpack : (config ) => {
config.resolve .alias = {
...config.resolve .alias ,
'@src' : path.resolve (__dirname, 'src' ),
'@public' : path.resolve (__dirname, 'public' ),
'@interfaces' : path.resolve (__dirname, 'interfaces' ),
'@config' : path.resolve (__dirname, 'config' )
};
return config;
}
2. Reference Boilerplate
Used Bombay Softwares' Next.js boilerplate for App Router project structure.
We created a plan in cursor to migrate the page router to app router structure.
Updated directory structure to match the boilerplate conventions
3. Stylesheet Migration
Updated stylesheets to work with newer version of Next.js
Used the official SASS migration tool:
npx sass-migrator division **/*.scss
Reference: https://sass-lang.com/documentation/cli/migrator/
5. Next.js Breaking Changes Resolution
Next.js 12 → 13
Change
Details
next/link
Before: 'textAfter: text No need for nested tag
Imports
Before: import { useRouter } from next/router'After: import { useRouter, usePathname, useSearchParams } from 'next/navigation`
Next.js 13 → 14
Node.js minimum: 18.17
No other breaking change in Frontend
Next.js 14 → 15
React bump to 19: Next.js 15 requires React 19
No other breaking change in Frontend
Next.js 15 → 16
Change
Details
Node.js minimum
20.9.0 - Node 18 is no longer supported
TypeScript minimum
5.1+
Default bundler
Turbopack becomes the default bundler
Middleware API
The old middleware.ts API is deprecated/replaced by proxy.ts as the primary network boundary file
6. Layouts and Route Groups
Created route groups as per Next.js Route Groups documentation:
Layouts:
Main Layout (src/app/layout.tsx) - Under app directory
Providers are added here
Script tags are added here
Auth Layout (src/app/(auth)/layout.tsx) - Under auth group
Navbar Layout (src/app/(auth)/(navbar)/layout.tsx) - Under navbar group
Navbar component is added here
Route Groups:
(auth) group - All routes that need authentication are grouped here
(navbar) group - All routes that need a Navbar are grouped here
Navbar group is nested under auth group
App Router Structure
7. Pages Redesign
Error Page (500)
Updated Error page to a newer UI
Attached Figma Screenshots in prompt to Cursor
Attached required assets to create the page
File: src/app/error.tsx
Expandable error details
Retry button
Help center links
Not Found Page (404)
Created a new 404 not found page
Used Figma Screenshots in prompt to Cursor
Attached required assets to create the page
File: src/app/not-found.tsx
Branded design with gene icon
Helpful navigation links
Appointment Booking Page Separation
Problem: Appointment booking had the same route but different UI for:
Logged in users
Logged out users
Solution: Separated the appointment booking page into 2 different pages:
Logged in users - Came under (auth) group: src/app/(auth)/book-appointment/
Logged out users - Came out of (auth) group: src/app/book-appointment/
8. Package Compatibility Updates
1. Package JSON Review
Once all breaking changes were resolved, started picking up packages one by one to ensure compatibility with:
Current Next.js version
Current Node.js version
2. MUI Migration (Major Change)
Problem: Two different versions of MUI were installed
Task: Remove older version and upgrade newer version to latest MUI
Migration Strategy:
Used Cursor to create a new text file with:
All usages of @mui/styles
References of older MUI versions
Attached the created file to a new chat
Asked Cursor to create a plan to upgrade all these changes and take references from the attached file.
Attached MUI changelogs along with it:
Breaking Changes:
Change
Before
After
@mui/styles
Supported
Removed in v7 - not supported in v6+ and fully removed in v7
Styling approach
makeStyles, withStyles
Migrated to styled from @mui/material/styles
Current MUI Packages:
{
"@emotion/react" : "^11.14.0" ,
"@emotion/styled" : "^11.14.1" ,
"@mui/icons-material" : "^7.3.5" ,
"@mui/material" : "^7.3.5" ,
"@mui/x-tree-view" : "^8.19.0"
}
3. Toast Notifications Migration
Problem: toasted-notes package stopped working with newer Next.js version
Solution:
Removed all usages of the library
We were already using custom functions to invoke toaster messages. We migrated any toaster code which was not using these functions.
This approach allowed changes to be made only in the custom function
Used react-hot-toast package as replacement
Before (toasted-notes):
toaster.notify (({ onClose } ) => (
<ErrorToaster onClose ={onClose} message ="Message string" />
), {
duration : 3000 ,
position : "top-right" ,
});
After (react-hot-toast with custom wrapper):
ShowErrorToaster ({
message : "Message string" ,
duration : 3000 ,
position : "top-right" ,
});
Implementation File: src/components/ui/ErrorToaster.tsx
4. Other Package Updates
Used similar approach for upgrading all other packages
Checked compatibility one by one
Used overrides in package.json for peer dependency conflicts
9. TypeScript Issues Resolution
1. Strategy for Bulk TypeScript Fixes
We used Cursor to create a new file that listed all type issues in the code and Group similar type issues together Where we have mentioned file paths and line number with the issues.
Provided the grouped issues list as context to Cursor to fix in all files with the same issue and included expected solution when sure.
This approach allowed fixing multiple files with the same pattern simultaneously.
2. TypeScript Configuration
{
"compilerOptions" : {
"target" : "ESNext" ,
"module" : "ESNext" ,
"moduleResolution" : "bundler" ,
"jsx" : "react-jsx" ,
"strict" : true ,
"noImplicitAny" : false
}
}
10. Middleware to Proxy Migration
New Proxy Implementation
The old middleware.ts API is deprecated/replaced by proxy.ts as the primary network boundary file.
Business logic is based on older _app.tsx file
File: src/proxy.ts
Key features:
Authentication token validation via cookies
Permission-based routing
JWT payload decoding (Edge Runtime compatible)
Protected route handling
Public path allowlisting
11. Lessons Learned
1. Use Plan Mode for Large Changes
For any large change, use Plan Mode in Cursor
Break down the upgrade into manageable phases
Validate with POC before full migration
2. Providing Relevant Context is Important
Provide relevant context for accurate results
Include changelogs and migration guides
Reference official documentation
Attach full repository context when needed
3. Example-Driven Prompts Yield Best Results
For best results, provide an example on how to fix the issues:
Example Prompt:
Remove all usages of toaster.notify in the frontend codebase.
Replace them with ShowErrorToaster imported from @src /ui/toaster.
**Replacement Rule :**
Any usage of :
`` `jsx
toaster.notify(({ onClose }) => (
<ErrorToaster onClose={onClose} message="Message string" />
), {
duration: 3000,
position: "top-right",
});
Must be replaced with:
` `` jsx
ShowErrorToaster ({
message : "Message string" ,
duration : 3000 ,
position : "top-right" ,
});
4. Systematic Package Updates
Update packages one by one
Verify compatibility with current Next.js and Node.js versions
Use overrides in package.json for peer dependency conflicts
5. Group Similar Issues Together
For TypeScript or lint errors, group similar issues
Fix all files with the same pattern in one go
Provide grouped context to AI for bulk fixes
Final Package Versions
Package
Old
New
@dnd-kit/core
^6.1.0
^6.3.1
@dnd-kit/sortable
^8.0.0
^10.0.0
@emotion/react
^11.9.0
^11.14.0
@emotion/styled
^11.8.1
^11.14.1
@mui/icons-material
^5.6.1
^7.3.5
@mui/material
^5.6.1
^7.3.5
@monaco-editor/react
^4.6.0
^4.7.0
@tanstack/react-query
—
v5.90.11
@tinymce/tinymce-react
^4.2.0
^6.3.0
ag-grid (community/enterprise/react)
32.0.2
^33.3.2
axios
^0.19.2
^1.13.2
react-bootstrap
^1.4.3
^2.10.10
react-hot-toast
—
v2.6.0
react-html-parser
^2.0.2
^5.2.10
react-markdown
^6.0.3
^10.1.0
react-mentions
^4.2.0
^4.4.10
react-modal
^3.11.2
^3.16.3
react-pdf
^5.7.2
^10.2.0
react-select
^3.2.0
^5.10.2
react-select-async-paginate
^0.5.3
^0.7.11
react-timezone-select
^2.1.5
^3.2.8
react-tooltip
^4.2.13
^5.30.0
reactflow
^11.11.3
^11.11.4
recharts
^2.0.8
^3.5.1
styled-components
^5.2.3
^6.1.19
yup
^0.27.0
^1.7.1
References