Blog

Blog · Error Fixes

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.

XCODX Team · 3 min read

"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

  1. Bare package? ls node_modules/<pkg> and confirm it is in package.json; if not, install it.
  2. Relative import? Verify the exact path and case against the real file.
  3. Using ESM or NodeNext? Confirm the relative import ends in .js.
  4. Using an alias like @/? Confirm it is defined in the bundler/runtime config, not only in tsconfig.
  5. 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?
macOS is case-insensitive, Linux (CI/Docker) is not. An import like ./header resolves Header.jsx locally but not on Linux. Match the filename case exactly.
Do I need the .js extension in imports?
In native ESM and TypeScript NodeNext, yes — relative imports must include the extension (./util.js), even when the source is a .ts file. Bare package specifiers must NOT include one.
Why does my @/ alias not resolve?
A tsconfig 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.