How to Fix "Module Not Found" (Node, webpack, Vite & Next.js)
Cannot find module, Can't resolve, Failed to resolve import — the resolver could not map your import to a real file or package. Here are the six causes and the exact fix for each.
"Module not found" means the resolver — Node's loader, or a bundler like webpack, Vite or Rollup — was asked to load an import and could not map it to a real file or an installed package. For bare specifiers like 'react' it walks node_modules; for relative ones like './util' it resolves against the importing file.
The 6 common causes and their fixes
1. The package is not installed
The dependency is imported but was never installed or is missing from package.json.
# imported but not installed
npm install express
# build-only tools:
npm install --save-dev vite
2. A wrong relative path or wrong depth
The ./ or ../ path is off by a directory level.
// file: src/components/Button.js, api.js is at src/api.js
// ❌ looks in src/components/
import { api } from './api';
// ✅ up one level
import { api } from '../api';
3. A case-sensitivity mismatch (Linux and CI only)
macOS and Windows are case-insensitive; Linux and Docker/CI are not. ./header will not find Header.jsx on a Linux build.
// file is Header.jsx
// ❌ fails on Linux/CI, works on Mac/Windows
import Header from './header';
// ✅ match the exact case
import Header from './Header';
4. A missing file extension in ESM / NodeNext
Native ESM and TypeScript’s NodeNext resolution require explicit extensions on relative imports.
// package.json has "type": "module"
// ❌
import { x } from './util';
// ✅ include .js (even when the source file is .ts under NodeNext)
import { x } from './util.js';
5. A path alias the bundler does not know
A tsconfig.json paths alias like @/* is understood by the editor and tsc, but not by the bundler or Node unless you mirror it there.
// vite.config.js
import path from 'node:path';
export default {
resolve: { alias: { '@': path.resolve(__dirname, './src') } }
};
// (webpack: resolve.alias · Jest: moduleNameMapper · or use vite-tsconfig-paths)
6. A deep import the package does not export
A package exports map can block a subpath that used to work. Import the public entry, or check the allowed subpaths.
// ❌ pkg only exports "." and "./sub"
import x from 'pkg/internal/x';
// ✅ use the public entry
import { x } from 'pkg';
Quick diagnostic checklist
- Bare package?
ls node_modules/<pkg>and confirm it is inpackage.json; if not, install it. - Relative import? Verify the exact path and case against the real file.
- Using ESM or NodeNext? Confirm the relative import ends in
.js. - Using an alias like
@/? Confirm it is defined in the bundler/runtime config, not only in tsconfig. - Recently changed deps or branches?
rm -rf node_modules && npm ci.
Frequently asked questions
Why does it work on my Mac but fail in CI?
./header resolves Header.jsx locally but not on Linux. Match the filename case exactly.Do I need the .js extension in imports?
./util.js), even when the source is a .ts file. Bare package specifiers must NOT include one.Why does my @/ alias not resolve?
paths alias only informs the editor and tsc. The bundler and Node do not read it — mirror the alias in your bundler config (Vite resolve.alias, webpack resolve.alias, Jest moduleNameMapper).Prototyping without the install/alias headaches? XCODX Studio resolves real npm packages in the browser via import maps — so import just works and you can focus on the code, not the resolver.