Keywords: Node.js | ES modules | node-fetch
Abstract: This article delves into common ES module import errors in Node.js environments, focusing on compatibility issues arising from node-fetch v3's transition to a pure ESM module. By analyzing a user case, it explains the error causes and provides three solutions: adding the type field to package.json, downgrading to v2, or using dynamic imports. The article contrasts these approaches with technical background, helping developers understand Node.js module system evolution and best practices.
Problem Background and Error Analysis
In modern JavaScript development, modularity is central to code organization. Node.js has long supported the CommonJS module system using the require() function for imports. However, with the adoption of the ECMAScript module (ESM) standard, Node.js introduced native ESM support starting from v12, leading to a dual module system. The error encountered by users with node-fetch v3—Error [ERR_REQUIRE_ESM]: Must use import to load ES Module—is a typical issue during this transition period.
Root Cause: ESM-Only Nature of node-fetch v3
node-fetch is a popular HTTP client library that mimics the browser Fetch API. In version 3.0.0, the library fully converted to an ESM-only module, meaning its package.json includes "type": "module". According to Node.js module resolution rules, when a package is marked as ESM, all .js files are treated as ES modules and can no longer be imported via require(). The user attempted to use the -r esm flag to preload the esm package for compatibility, but the esm package is designed to simulate ESM in older Node versions and is ineffective for native ESM packages like node-fetch v3.
Solution 1: Enable Native ESM Support
The optimal solution is to leverage Node.js's native ESM support. First, remove the unnecessary esm package dependency, as Node v14 already includes built-in ESM functionality. Then, add the "type": "module" field to the project's package.json, as shown in this example:
{
"name": "my-project",
"type": "module",
"dependencies": {
"node-fetch": "^3.0.0"
}
}
After this configuration, running node server.js directly will correctly import node-fetch. This method utilizes Node.js's modern features, avoids compatibility layers, offers better performance, and adheres to standards.
Solution 2: Downgrade to CommonJS Version
If the project is based on a CommonJS architecture or requires backward compatibility, downgrading node-fetch to v2 is a viable option. Version 2 is built with CommonJS and supports require() imports. Install the specified version via a package manager:
npm install node-fetch@^2.6.6
or
yarn add node-fetch@^2.6.6
Then, use require() in the code:
const fetch = require("node-fetch");
Note that while v2 is no longer actively developed, it receives critical security updates, making it suitable for stable projects.
Solution 3: Dynamic Imports and TypeScript Adaptation
For mixed module environments, the dynamic import() function can asynchronously load ESM modules. This is effective when calling ESM packages from CommonJS files:
const fetch = (...args) => import('node-fetch').then(({ default: fetch }) => fetch(...args));
If using TypeScript, explicitly define parameter types to avoid type errors:
import { RequestInfo, RequestInit } from "node-fetch";
const fetch = (url: RequestInfo, init?: RequestInit) =>
import("node-fetch").then(({ default: fetch }) => fetch(url, init));
This approach provides flexibility but adds asynchronous complexity.
Technical Background and Best Practices
The evolution of Node.js's module system reflects the development of the JavaScript ecosystem. Key differences between CommonJS and ESM include: CommonJS loads synchronously, while ESM supports static analysis and asynchronous loading. In package.json, the "type" field defaults to "commonjs"; when set to "module", all .js files are parsed as ES modules, and .cjs files remain CommonJS. For new projects, using ESM is recommended for better tree-shaking optimization and browser compatibility; for legacy systems, gradual migration or dynamic imports can facilitate transition.
Conclusion
The ESM-only nature of node-fetch v3 represents progress in module system standardization but also introduces compatibility challenges. By analyzing error messages, developers should understand that the root cause lies in the package metadata's type setting. Solutions should be chosen based on project context: enabling native ESM is the best practice, downgrading suits stable needs, and dynamic imports offer a transitional approach. Mastering these concepts helps efficiently handle module issues in the Node.js ecosystem and promotes code modernization.