Node.js ESM "ERR_MODULE_NOT_FOUND" on Extensionless Imports
Native ESM does not do CommonJS-style extension guessing. A relative import without a file extension that worked under CJS (or a bundler) fails under Node ESM with ERR_MODULE_NOT_FOUND.
What this error means
After switching to "type": "module" or running compiled ESM, imports like import { x } from "./util" fail with ERR_MODULE_NOT_FOUND, even though ./util.js exists. The same source ran fine under CommonJS or through a bundler.
node:internal/modules/esm/resolve:... Error [ERR_MODULE_NOT_FOUND]:
Cannot find module '/app/src/util' imported from /app/src/index.js
Did you mean to import './util.js'?Common causes
Extensionless relative import under ESM
CommonJS and bundlers resolve ./util to ./util.js for you. Node’s native ESM resolver does not - it requires the full specifier including the .js (or .mjs) extension.
TypeScript output without rewritten extensions
TS source often imports ./util (no extension); when emitted as ESM, Node needs ./util.js. Without the right moduleResolution/rewrite, the emitted import is unresolvable.
How to fix it
Add explicit file extensions
Use the full extension in relative ESM imports.
// before
import { x } from './util';
// after
import { x } from './util.js';Configure TypeScript for Node ESM
Use a Node-aware module resolution so TS expects/keeps the .js extension in emitted imports.
// tsconfig.json
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext"
}
}How to prevent it
- Always include file extensions in relative ESM imports.
- Use
nodenextmodule resolution for TypeScript targeting Node ESM. - Lint with a rule that enforces explicit extensions in ESM.