Next.js 16 came out in October 2025. A year later it is the version most new projects start on, and the version a lot of working sites still have not moved to. This site moved this month. It is a real app, not a starter: the App Router, a Postgres database through TypeORM, a credentials-based admin panel, and a Docker image that runs on a small VPS.
The official upgrade guide is good and the codemod does most of the typing. What the guide cannot tell you is which of its changes will hit your particular stack. Two problems broke this site's production build in ways a dev server never showed. This is what broke, why, and what I would check before you upgrade yours.
Turbopack builds your app differently
The headline change is that next build uses Turbopack by default. Development already did; now production does too. For most code that is invisible. For code that relies on two routes sharing the same instance of a module, it is not.
After the upgrade, the production build served every public page fine, and admin sign-in failed with this:
No metadata for "Admin" was foundThe cause is how the database connection and the bundles meet. The TypeORM DataSource is created once and shared through globalThis, so whichever route connects first registers its entity classes. Turbopack gives each route its own copy of those classes. In production the first request is usually a public page, so the connection held the public page's Admin class, and the auth route asked for a repository with its own, different Admin class. TypeORM compares classes by identity, so as far as it was concerned that entity had never been registered.
webpack, the builder Next.js 15 used for production, shared one module instance across routes, which is why the same code had worked for months.
The fix is one line per lookup: ask for the repository by name instead of by class.
// Breaks when the route that connected first holds a different copy of the class
const admins = ds.getRepository(Admin);
// Works from any route: TypeORM matches the entity by its name
const admins = ds.getRepository<Admin>('Admin');The public loaders had already been switched to name lookups months earlier, after the same error showed up in development. The auth route was the one place still passing a class, and nothing exercised it until the production build did.
The lesson is broader than TypeORM. If anything in your app keeps state in a module-level singleton and compares classes or object identity across routes, test it on the production build. next dev will not show you this.
The Docker image lost its database driver
This one was not strictly Next.js. It came from upgrading TypeORM to 1.x in the same round, and it only surfaced because of how Next.js packages a standalone build. The two together are worth knowing about.
The site ships as output: 'standalone'. Next.js traces which files the server actually imports and copies only those into .next/standalone, which keeps the Docker image small. TypeORM 1.x loads its database driver with a dynamic require(name). Output tracing cannot follow a require whose argument is a variable, so the trace quietly left pg out. Every page that touched the database threw DriverPackageNotInstalledError.
The fix is to import the driver yourself and hand it over, which puts it back in the trace:
import pg from 'pg';
new DataSource({
type: 'postgres',
driver: pg,
// ...
});It never reached visitors, because CI builds the Docker image and runs a smoke test against it before anything deploys. It had already earned its place once, after an earlier standalone surprise: images uploaded at runtime into public/ were not served, and the test grew a check for that too. If your deploy is a standalone build and your CI does not boot it, I would add that before upgrading anything.
Middleware is now called proxy
middleware.ts is deprecated in favour of proxy.ts, and the exported function is renamed to match. The codemod does the rename. One difference matters if you relied on the Edge runtime: proxy always runs on Node.js, and the guide says to keep middleware for now if you need Edge. The behaviour this site relies on, guarding /admin before a request reaches the page, works the same.
The name is more honest about what the file does: it sits in front of your routes and decides what to do with a request.
I still check the session again inside every admin page and server action. A guard in front of the routes is a convenience, not the only lock on the door.
next lint is gone
next lint was removed. You run the ESLint CLI directly, with a flat config. The codemod writes one for you; this site's is core web vitals, TypeScript and Prettier in eslint.config.mjs, and npm run lint calls ESLint itself.
The practical difference is that ESLint now lints whatever its config says, not what Next.js decided to point it at. The first run after the switch walked into tool folders that had never been linted before, and they needed adding to the ignores.
The small breaking changes
Most of the remaining changes are mechanical, and the codemod handles them:
params,searchParams,cookies()andheaders()are async. Await them.revalidateTagtakes a second argument, a cache profile such as'max'. The single-argument form is deprecated and fails type checking.- The minimum Node.js is 20.9, and the supported browsers start at Safari 16.4 and Chrome 111.
- If you have a custom
webpackconfig,next buildfails on purpose rather than silently ignoring it. You either migrate it or build with--webpack.
None of these are hard. They are just noisy, which is a good reason to land the upgrade on its own and not inside a feature branch.
The React Compiler is stable, and still a choice
Next.js 16 made built-in support for the React Compiler stable, after the compiler's own 1.0 release. It memoizes components for you, so most hand-written useMemo and useCallback calls become unnecessary.
It is off on this site for now. If you turn it on, two things are worth knowing first. The upgrade guide warns that builds and dev compiles get slower, because the compiler runs through Babel. And eslint-plugin-react-hooks 7 ships rules that only make sense with the compiler on; this site has those three switched off, to be switched back on the day the compiler is.
What I would check before you upgrade
If you are still on Next.js 15, this is the short list I would work through:
- Test the production build, not dev. Run
next buildandnext start, then click through the parts of the app that only some users reach: sign-in, admin, webhooks, anything behind auth. - Look for shared singletons. Database connections, caches and clients stored on
globalThisare fine. Comparing classes across routes is not. - Boot the standalone output in CI. If you deploy
.next/standalone, start it and hit a few routes before you deploy. Dynamic requires are where tracing loses files. - Upgrade one thing at a time. The driver problem came from TypeORM, not Next.js, and it was easy to pin down only because the dependency bumps were separate commits.
- Run the codemod, then read its diff. It renames and rewrites correctly. It cannot know which of your routes depend on what it changed.
A year on, Next.js 16 is a better default than 15: faster builds, clearer names, and a cache API that says what it does. The upgrade itself is mostly typing. The work is in the few places where your app depended on how the old builder happened to behave, and the production build is the only place that will show you where those are.