Tình Huống
2 giờ sáng. Deploy vừa thất bại, hoặc build local vừa nổ tung sau khi nâng version dependency. Stack trace trông như thế này:
Error [ERR_REQUIRE_ESM]: require() of ES Module /path/to/node_modules/node-fetch/src/index.js not supported.
/path/to/node_modules/node-fetch/src/index.js is an ES module file as it is a .js file whose nearest parent package.json contains "type": "module" which defines all .js files in that package as ES modules.
Instead either rename /path/to/node_modules/node-fetch/src/index.js to end in .cjs, change the requiring code to use import(), or remove "type": "module" from /path/to/node_modules/node-fetch/src/index.js.
at Object. (/your/project/src/api.js:3:18)
Code của bạn không thay đổi gì — chỉ có version package thay đổi thôi. Chào mừng bạn đến với bức tường tương thích CommonJS vs ESM.
Chuyện Gì Đang Xảy Ra
Node.js hỗ trợ hai hệ thống module:
- CommonJS (CJS): dùng
require()/module.exports— cách truyền thống của Node.js - ESM (ES Modules): dùng
import/export— chuẩn hiện đại
Vấn đề ở đây là: CJS không thể require() một module ESM. Đơn giản vậy thôi. Từ khoảng năm 2021, nhiều package phổ biến như node-fetch v3, chalk v5, nanoid v4, got v12, và execa v6 đã bỏ hoàn toàn hỗ trợ CJS và chuyển sang ESM-only. Chỉ cần nâng cấp một trong số này, bất kỳ project nào vẫn dùng require() sẽ lập tức gặp vấn đề.
Xác Định Nguyên Nhân Gốc Rễ Trước
Trước khi chọn cách sửa, hãy xác nhận project của bạn đang dùng hệ thống module nào:
cat package.json | grep '"type"'
- Không có trường
typehoặc"type": "commonjs"→ project của bạn đang dùng CJS "type": "module"→ project của bạn đang dùng ESM
Sau đó kiểm tra package gây lỗi:
cat node_modules/node-fetch/package.json | grep '"type"'
Nếu nó hiển thị "type": "module", package đó là ESM-only — bạn không thể require() nó. Giờ bạn đã biết vấn đề nằm ở đâu rồi. Chọn cách sửa bên dưới.
Sửa Nhanh: Ghim Lại Version CJS Cuối Cùng
Cần chạy được trong 5 phút tới? Ghim package về version CommonJS-compatible cuối cùng của nó. Không cần thay đổi code.
# node-fetch: version CJS cuối cùng là v2
npm install node-fetch@2
# chalk: version CJS cuối cùng là v4
npm install chalk@4
# nanoid: version CJS cuối cùng là v3
npm install nanoid@3
# got: version CJS cuối cùng là v11
npm install got@11
# execa: version CJS cuối cùng là v5
npm install execa@5
Kiểm tra xem đã hoạt động chưa:
node -e "const fetch = require('node-fetch'); console.log(typeof fetch);"
Kết quả phải in ra function. Vậy là xong. Triển khai thôi.
Sửa Lâu Dài — Cách 1: Dùng Dynamic import()
Muốn dùng version mới nhất của package mà không cần chuyển đổi toàn bộ project? Dynamic import() chính là cầu nối. File CJS có thể gọi import() — chỉ là không thể dùng cú pháp import tĩnh ở đầu file.
// Trước (lỗi)
const fetch = require('node-fetch');
// Sau (hoạt động trong file CJS)
async function fetchData(url) {
const { default: fetch } = await import('node-fetch');
const res = await fetch(url);
return res.json();
}
Chú ý cú pháp destructuring { default: fetch }. Khi truy cập default export của ESM qua dynamic import từ CJS, giá trị sẽ nằm ở thuộc tính default — không phải trực tiếp là giá trị.
Kiểm tra nhanh để xác nhận hoạt động:
node -e "
async function test() {
const { default: fetch } = await import('node-fetch');
const res = await fetch('https://httpbin.org/get');
console.log(res.status);
}
test();
"
Sửa Lâu Dài — Cách 2: Chuyển Đổi Project Sang ESM
Để có giải pháp sạch về lâu dài, hãy migrate toàn bộ project sang ESM. Tốn công hơn lúc đầu — nhưng sẽ loại bỏ hoàn toàn vấn đề tương thích.
Bước 1: Cập nhật package.json
{
"type": "module"
}
Bước 2: Thay require/module.exports bằng import/export
// Trước (CJS)
const express = require('express');
const { readFileSync } = require('fs');
module.exports = { myFunction };
// Sau (ESM)
import express from 'express';
import { readFileSync } from 'fs';
export { myFunction };
Bước 3: Sửa __dirname và __filename (chúng không tồn tại trong ESM)
// Thêm đoạn này vào đầu các file cần dùng __dirname
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
Bước 4: Sửa các dynamic require
// Trước
const config = require(`./configs/${env}.js`);
// Sau
const { default: config } = await import(`./configs/${env}.js`);
Chạy project và xem có lỗi gì không:
node src/index.js
Có thể sẽ còn một vài trường hợp đặc biệt nữa — JSON imports, require.resolve, và một số package cũ chưa hỗ trợ ESM. Gặp đâu sửa đó.
Sửa Lâu Dài — Cách 3: Dùng Bundler hoặc Transpiler
Migration thủ công quá rủi ro? Để bundler xử lý việc chuyển đổi CJS/ESM thay bạn.
# esbuild — lựa chọn nhanh nhất
npm install -D esbuild
npx esbuild src/index.js --bundle --platform=node --outfile=dist/index.cjs
# tsx — phù hợp cho TypeScript project
npm install -D tsx
npx tsx src/index.ts
Người dùng TypeScript có thể giữ output CJS trong khi viết theo cú pháp ESM-style:
// tsconfig.json
{
"compilerOptions": {
"module": "CommonJS",
"esModuleInterop": true
}
}
Trường Hợp Đặc Biệt: Chạy Trong Jest
Jest mặc định dùng CJS mode, vì vậy các package ESM cũng sẽ làm hỏng test của bạn. Có hai lựa chọn:
# Lựa chọn 1: bật hỗ trợ ESM trong Jest
NODE_OPTIONS='--experimental-vm-modules' npx jest
# Lựa chọn 2: yêu cầu Jest transform package ESM
module.exports = {
transformIgnorePatterns: [
'node_modules/(?!(node-fetch|chalk|nanoid)/)',
],
};
Lựa chọn 1 đơn giản hơn để thiết lập. Lựa chọn 2 cho phép kiểm soát chi tiết hơn khi có nhiều package ESM-only cùng lúc.
Kiểm Tra Lại
Chạy ba kiểm tra này sau khi áp dụng bất kỳ cách sửa nào:
# 1. Chạy entry point trực tiếp
node src/index.js
# 2. Chạy test suite
npm test
# 3. Kiểm tra nhanh import cụ thể
node -e "import('node-fetch').then(m => console.log('OK:', typeof m.default))"
Không còn ERR_REQUIRE_ESM nào trong output? Xong rồi đó.
Nên Chọn Cách Nào
- Cần sửa trong 5 phút: ghim về version CJS cuối cùng
- Muốn dùng package mới nhất, ít refactor nhất: dùng dynamic
import() - Project mới hoặc sẵn sàng refactor: chuyển đổi project sang ESM
- TypeScript project: đặt
"module": "CommonJS"trong tsconfig +esModuleInterop: true

