moduleResolution 'NodeNext' Breaks Next.js Type Imports: Use 'bundler'
You did the responsible thing. You read the modern TypeScript guidance, saw that "moduleResolution": "nodenext" is the correct, spec-accurate setting for ESM projects, set it in your Next.js tsconfig.json, and instantly your editor lit up red on Next.js's own imports. next/navigation, next/headers, next/link, all reported as if they did not exist. The framework you are using cannot find its own modules, because you followed TypeScript's most-correct advice.
This is one of those maddening cases where "most correct" and "actually supported" diverge. NodeNext is stricter than the resolver Next.js is built around, and Next.js does not (or did not) publish type declarations that satisfy that stricter algorithm for every subpath. The fix is not to fight it. It is to use the module resolution mode Next.js actually recommends for App Router projects: "bundler".
for a Next.js App Router project, set "moduleResolution": "bundler" in tsconfig.json, not "nodenext". Next.js compiles through a bundler, so bundler matches how modules are actually resolved and it does not impose NodeNext's strict subpath rules that Next.js's type exports do not fully satisfy. Set "module": "esnext" alongside it. After changing it, delete next-env.d.ts and run next dev to regenerate types.
The symptom: Next.js built-in imports report "cannot find module"
With moduleResolution: "nodenext", imports that obviously exist get flagged:
Cannot find module 'next/navigation' or its corresponding type declarations. ts(2307)
Cannot find module 'next/headers' or its corresponding type declarations. ts(2307)
The code runs fine, next dev works, the app compiles. It is purely the TypeScript language server (and tsc --noEmit) that refuses to resolve the types, so your editor is a sea of red and your CI type-check fails while the app itself is healthy. That gap, working runtime, failing types, is the signature of a module-resolution mismatch rather than a real missing dependency.
Why NodeNext breaks Next.js's own types
TypeScript's moduleResolution setting decides how it looks up an imported module's files and types. The three that matter here behave very differently:
| Mode | How it resolves | Fit for Next.js |
|---|---|---|
node (legacy "node10") | Old CommonJS-style lookup, no exports map enforcement | Works but deprecated; TS is phasing it out |
nodenext / node16 | Strict Node ESM rules: honours package.json exports, requires precise conditions, distinguishes ESM vs CJS by extension | Too strict; flags Next.js subpaths as missing |
bundler | Resolves the way a bundler does: respects exports but without NodeNext's extension and condition strictness | Recommended by Next.js for App Router |
Under nodenext, TypeScript resolves subpath imports like next/navigation strictly through the exports field in next's package.json and expects the type conditions to line up exactly with Node's ESM algorithm. When Next.js's published exports map and type declarations do not fully satisfy that strict algorithm for every subpath, NodeNext concludes the module has no types and reports ts(2307). It is not that the types are absent; it is that NodeNext will not accept the way they are exposed.
The important thing to understand: NodeNext is designed for code that Node itself will run as ESM, where extension and condition precision genuinely matter. Next.js code does not run through Node's raw resolver; it runs through Next.js's compiler and bundler. So applying Node's strictest resolution to it is answering a question the runtime never asks. That is exactly the gap bundler exists to fill.
Step 1: Set the recommended tsconfig for App Router
Use the resolution mode Next.js recommends. The relevant lines:
{
"compilerOptions": {
"target": "ES2017",
"module": "esnext",
"moduleResolution": "bundler",
"jsx": "preserve",
"esModuleInterop": true,
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"incremental": true,
"resolveJsonModule": true,
"isolatedModules": true,
"plugins": [{ "name": "next" }],
"paths": { "@/*": ["./*"] }
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
Note two pairings that matter: "module": "esnext" goes with "moduleResolution": "bundler" (you cannot pair bundler with "module": "commonjs"), and ".next/types/**/*.ts" must be in include so Next.js's generated route types are picked up. This is, in fact, close to what create-next-app scaffolds by default.
Step 2: Regenerate the Next.js types
Next.js maintains next-env.d.ts, which references its ambient type declarations. After changing resolution, force it to regenerate:
rm next-env.d.ts
npm run dev # or: next dev - Next.js recreates next-env.d.ts on boot
Do not hand-edit next-env.d.ts; Next.js overwrites it. Deleting and letting it regenerate is the supported move.
Step 3: Restart the TypeScript language server
Your editor caches resolution results, so it can keep showing the old errors after a correct config change. In VS Code, open the command palette and run TypeScript: Restart TS Server. The red squiggles on next/navigation should clear immediately.
Verification
Prove it with the type checker, not just the editor:
npx tsc --noEmit
A clean exit (no ts(2307) for next/* modules) means resolution is fixed. If your project has a lint/type CI step, that same command is what was failing before; it should now pass. Runtime was never broken, so next build was already succeeding, the point is that types and CI now agree with it.
What people get wrong
"NodeNext is the correct setting, so I must make it work." Correct for what? NodeNext is correct for code Node runs directly as ESM. Next.js is a bundled framework; its supported answer is bundler. Insisting on NodeNext here is applying the right rule to the wrong target, and you will keep fighting subpath resolution forever.
"Downgrade to moduleResolution: "node"." That silences the errors but pins you to the legacy "node10" algorithm TypeScript is actively deprecating, and it does not respect exports maps, so it will misresolve modern packages elsewhere. bundler is the modern replacement that actually fits; do not reach back to the deprecated one.
"Add a manual declare module 'next/navigation' shim." This masks the real types with an empty stub, so you lose all type-checking and autocomplete on those modules. You are throwing away the very thing TypeScript is for to work around a one-line config mistake.
"It is a bug in Next.js, I'll wait for a fix." Whether or not the exports map is imperfect, the supported configuration is documented and available today. There is nothing to wait for. Related ESM-versus-CommonJS friction shows up elsewhere too, for example the ERR_REQUIRE_ESM class of errors on upgrade.
When it is still broken
- Errors persist after the change. You almost certainly did not restart the TS server, or your editor is reading a different
tsconfig.json(monorepos often have several). Confirm which config the language server picked up. - Only your path aliases fail, not
next/*. That is a separate issue withpathsand workspace packages, covered in TypeScript path aliases breaking under transpilePackages. - You genuinely need NodeNext for a Node-side package in the same repo. Split it: give the Node/server package its own
tsconfig.jsonwithnodenextand keep the Next.js app onbundler. One resolution mode does not have to rule the whole monorepo. .next/typesroute errors appear. Make sure".next/types/**/*.ts"is inincludeand that you have runnext devornext buildat least once so those generated files exist.
The lesson to carry: TypeScript's "most correct" module resolution and your framework's "actually supported" module resolution are not always the same setting, and when they conflict, the framework's compiler wins because it is what runs your code. For Next.js App Router that answer is bundler, and it is not a compromise, it is the mode built for exactly this situation.
Frequently asked questions
- Why does moduleResolution: nodenext break Next.js imports like next/navigation?
- NodeNext applies Node's strict ESM resolution, resolving subpaths through the package's exports map with exact condition and extension rules. Next.js's published type exports do not fully satisfy that strict algorithm for every subpath, so TypeScript concludes the module has no types and reports ts(2307). The code still runs, because Next.js resolves modules through its own bundler, not Node's raw resolver.
- What moduleResolution should a Next.js App Router project use?
- Use 'bundler', paired with 'module': 'esnext'. Next.js recommends bundler for App Router because it resolves modules the way Next.js's compiler actually does, respecting exports maps without imposing NodeNext's strict extension and condition rules. This is also what create-next-app scaffolds by default.
- I changed moduleResolution to bundler but the errors are still showing. Why?
- Your editor's TypeScript language server has cached the old resolution. Run 'TypeScript: Restart TS Server' from the command palette, and delete next-env.d.ts then run next dev to regenerate Next.js's ambient types. Also confirm the language server is reading the tsconfig.json you edited, which matters in monorepos with several config files.
- Should I use moduleResolution 'node' instead to fix this?
- No. 'node' (the legacy node10 algorithm) silences the errors but is deprecated by TypeScript and does not respect exports maps, which causes misresolution of other modern packages. 'bundler' is the modern replacement designed for bundled frameworks like Next.js and is the supported choice.