Upgrading Our Monorepo to Next.js 16.1.6
Last October 2025, Vercel released Next.js 16. It provides the latest improvements to Turbopack, caching, and the Next.js architecture.
And recently, I upgraded several of my company's Next.js applications in a monorepo from Next.js 15.5 to 16.1 (released in December 2025).
While the upgrade introduces some breaking changes, the actual impact on our projects was limited and manageable. The upgrade helped reduce configuration ambiguity, clarify request handling, and lower future upgrade risk.
Why upgrade to Next.js 16?
Next.js 16 doesn't drastically change how you build apps, but it locks in conventions that were already forming in earlier versions.
For our projects, the motivation wasn't chasing new features, it was alignment and predictability:
- Turbopack becoming the default: Removes ambiguity between dev and CI builds and reduces script-level configuration.
- Clearer request-layer conventions: Renaming Middleware to Proxy makes intent more explicit and avoids overloading the term “middleware.”
- Async request derived props: Making
paramsandsearchParamsasync clarifies rendering behavior and removes implicit assumptions. - Cleaner separation of concerns in config: Framework config, linting, and tooling are now more clearly separated.
Overall, upgrading earlier avoids carrying version differences across multiple apps and deployment pipelines.
The monorepo setup (before the upgrade)
The monorepo is structured as follows (simplified):
monorepo/
├── apps/
│ ├── web/
│ ├── valuation/
│ ├── landing/
│ └── playground/
Each app had its own package.json, build/dev scripts, and next.config.*. Some apps had custom Next config and request-layer concerns (e.g. security headers / middleware), and at least one had a custom webpack config.
Versions:
- Next.js: 15.5.10
- React: 19.2.x
The upgrade, step by step
1) Bumped Next.js to 16.1.6
I upgraded next from 15.5.10 to 16.1.6 in each app individually.
react was already aligned on 19.2.x, so no changes were needed there.
I ran upgrades app-by-app rather than from the repo root to keep failures isolated and easier to reason about.
2) Removed explicit Turbopack flags (--turbo)
In Next.js 16, Turbopack is the default.
That made scripts like:
next dev --turbo
next build --turbo
unnecessary. I simplified them to:
next dev
next build
3) Next config changes
ESLint config no longer in next.config.*
For apps that had eslint config embedded in next.config.*, such as:
eslint: {
ignoreDuringBuilds: true,
}
I removed it.
Linting is now handled explicitly via Biome configuration and CI, rather than being embedded in the build step.
Custom webpack() config and Turbopack
For apps using a custom webpack() function, Next.js 16 may fail unless Turbopack is explicitly acknowledged.
The fix was adding:
turbopack: {}
This makes the intent explicit and avoids build-time errors.
If Turbopack compatibility becomes an issue, forcing webpack via next build --webpack is still an option, but I treated that as a fallback rather than the default path.
4) Middleware → Proxy migration
Next.js 16 renames the middleware convention to "proxy".
For the web app, I migrated:
middleware.tstoproxy.tsexport function middleware(...)toexport function proxy(...)
The matcher config stays the same, behavior remains the same, only the convention changes.
5) Async searchParams / params in App Router pages
In Next.js 16, params and searchParams passed to pages are now Promises.
This primarily affects Client Component pages that receive these props directly.
Before:
searchParams.fileName
After:
const resolvedSearchParams = use(searchParams)
Pages using useSearchParams() from next/navigation are not affected.
This was one of the few changes that required code edits, but only in a small number of pages.
Using AI agents during the upgrade (DevTools MCP)
While doing the upgrade, I also used AI agents via Next.js DevTools MCP to support the process.
The goal wasn't to automate the upgrade or blindly apply changes, but to reduce friction in areas that are mostly mechanical or verification-heavy.
In practice, I used AI agents to:
- Scan upgrade diffs and changelogs and identify what was relevant to my company's setup
- Review
next.config.*and scripts across apps to spot patterns that needed updates - Validate mechanical changes such as Middleware → Proxy migration
- Cross-check assumptions around App Router behaviors and async props
All changes were still verified manually. The AI agent acted as a second set of eyes, not a replacement for testing or code review.
I still relied on local builds, runtime checks, and code review to validate all changes.
This was particularly helpful in a monorepo context, where similar issues appear across multiple apps with small variations.
Why the breaking changes were limited for the projects
Next.js 16 does introduce breaking changes, but the impact on the projects was limited in practice.
This is largely because the projects already:
- Use App Router consistently
- Avoid heavy reliance on legacy middleware patterns
- Keep routing and request handling explicit
In addition, the upgrade process itself was structured:
- Used AI agents to reduce oversight during repetitive checks
- App-by-app validation
- Clear separation between mechanical and behavioral changes
As a result, most issues were resolved at the configuration or page level, without requiring architectural changes.
Benefits
Beyond the version bump, upgrading to Next.js 16 brought a few practical improvements.
One improvement that is easy to overlook is the clearer separation between development output and production build output in Next.js 16. As mentioned in the Next.js 16 blog: Significant performance optimizations for next dev and next start commands, this is a big improvement.
Other than that, with all apps running on the same version and conventions, it's easier to standardise tooling, CI pipelines, and internal patterns across the monorepo.
And by adopting Next.js 16 conventions early (Proxy, async request props, Turbopack defaults, etc) reduces the likelihood of larger breaking changes in future upgrades.
Final thoughts
Upgrading from Next.js 15 to 16 across my company's projects wasn't a zero-effort change, but it also wasn't disruptive.
Most of the work was mechanical, scoped, and aligned with conventions that the projects were already following. The upgrade helped reduce configuration ambiguity, clarify request handling, and lower future upgrade risk.
If you're planning a similar upgrade, I hope this gives you a clearer idea of what to expect.
Thank you for reading!