Why Node 18 & TS 4.7 Still Struggle with ESM? A Deep Dive into CommonJS vs ESM
This article explains why, despite years of ESM usage, Node 18 and TypeScript 4.7 still encounter import errors, explores the differences between CommonJS and ESM module resolution, and shows how to configure tsconfig and package.json to make Node correctly run ESM code.
Even though we have been writing ESM code for years, Node 18 and TypeScript 4.7 still claim to "support" ESM, leading to confusing errors.
Demo Project
// src/a.ts
export const a = 123;
// src/index.ts
import { a } from './a';
import fetch from 'node-fetch';
console.log(a, fetch);
// tsconfig.json
{
"compilerOptions": {
"moduleResolution": "node",
"module": "commonjs",
"outDir": "dist"
}
}
// package.json
{
"name": "node-test",
"version": "1.0.0",
"main": "dist/index.js"
}The current Node backend project uses the following basic configuration:
TypeScript for type checking.
TypeScript's module resolution set to Node (CommonJS).
TypeScript transpiles to CommonJS format.
Problem Origin: node-fetch
Starting with node-fetch@3, the library only provides an ESM build. Running the compiled code results in:
/Users/futengda/Desktop/playground/node-test/dist/index.js:4
var node_fetch_1 = require("node-fetch");
^
Error [ERR_REQUIRE_ESM]: require() of ES Module ... not supported.
Instead change the require ... to a dynamic import() which is available in all CommonJS modules.In short, require() cannot load an ESM module.
Reference: https://nodejs.org/api/esm.html#require
Why does my code use ESM but still get blocked?
Node Doesn't Recognize the File as ESM
Node still processes the file with its CommonJS loader ( node:internal/modules/cjs/loader), so we must explicitly tell Node the file is ESM, either by renaming it to .mjs or adding "type": "module" to package.json. We choose the latter.
After adding type: "module" and recompiling, we get a new error:
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/Users/futengda/Desktop/playground/node-test/dist/a' imported from /Users/futengda/Desktop/playground/node-test/dist/index.jsWhy does Node still cannot find the module even when it treats the file as ESM?
Node Doesn't Understand This ESM Format
ESM implementations differ in how they resolve the module specifier after from. There are two broad categories:
Runtime implementations
Node's ESM runtime
Browser's ESM runtime
Compile‑time implementations
Rollup/Webpack/ESBuild bundling capabilities
TypeScript/Babel/SWC transpilation capabilities
Vite's bare‑import‑to‑URL conversion
The key difference lies in how the module specifier is resolved.
Runtime resolvers follow the ESM standard strictly; Node also supports bare specifiers via the exports field in package.json. Browsers can use import‑maps for similar purposes.
Reference: https://nodejs.org/api/esm.html#import-specifiers
When we set module to esnext in tsconfig.json, TypeScript emits ESM code but still uses CommonJS‑style resolution, because historically the Node ecosystem is built around CommonJS.
Why does it still use the old approach? Because front‑end engineering has long relied on Node's CommonJS conventions.
Therefore, telling Node to run a file that was originally intended to be resolved with CommonJS causes failures.
Generating Node‑compatible ESM
TypeScript 4.7 introduces a proper ESM mode. By setting module to nodenext (or node16), TypeScript emits code that follows Node's ESM resolution rules.
Running the compilation again yields:
error TS2835: Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './a.js'?
1 import { a } from './a'
~~~~~
Found 1 error in src/index.ts:1TypeScript correctly flags the missing .js extension. Adding the extension fixes the issue:
// src/index.ts
import { a } from './a.js';Now the project runs without the previous ESM errors.
Conclusion: Why Was ESM Re‑released?
There are two flavors of ESM:
ESM format that uses CommonJS‑style module resolution.
ESM format that follows the native ESM module resolution.
Understanding which environment your ESM code runs in is essential for avoiding the pitfalls described above.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
