Sửa lỗi ERR_UNSUPPORTED_DIR_IMPORT Khi Import Thư Mục trong Node.js ES Modules

intermediate💚 Node.js2026-05-17| Node.js 12+ với ES Modules (ESM), mọi hệ điều hành (Linux, macOS, Windows). Xảy ra khi dùng file .mjs hoặc khai báo "type": "module" trong package.json.

Error Message

Error [ERR_UNSUPPORTED_DIR_IMPORT]: /app/src/utils is not supported resolving ES modules imported from /app/src/index.mjs. Did you mean to import /app/src/utils/index.js?
#esm#import#modules#nodejs#es-modules

Lỗi Gặp Phải

Error [ERR_UNSUPPORTED_DIR_IMPORT]: /app/src/utils is not supported resolving ES modules imported from /app/src/index.mjs. Did you mean to import /app/src/utils/index.js?

Nỗi đau kinh điển khi migration. Thứ hoạt động hoàn hảo trong CommonJS lại nổ tung ngay khi bạn chuyển sang ES Modules. Hoặc bạn thêm "type": "module" vào package.json, nhấn save, và nhìn một nửa số import bị vỡ cùng lúc.

Nguyên Nhân

CommonJS khá dễ tính. Bạn có thể viết require('./utils') và Node.js sẽ âm thầm resolve nó thành ./utils/index.js. Hành vi đó đã biến mất trong ESM — spec yêu cầu specifier phải hoàn toàn tường minh. Node.js sẽ không tự đoán file cho bạn.

Vì vậy import này:

import { formatDate } from './utils';

...sẽ ném ra lỗi ERR_UNSUPPORTED_DIR_IMPORT./utils là một thư mục. Node.js thấy thư mục, từ chối tự chọn entry point, và dừng hẳn.

Cách Sửa Từng Bước

Cách 1: Thêm Đường Dẫn File Tường Minh (Nhanh Nhất)

Trỏ import đến file thực tế:

// Trước (lỗi trong ESM)
import { formatDate } from './utils';

// Sau (đã sửa)
import { formatDate } from './utils/index.js';

Extension .js là bắt buộc — kể cả với output được compile từ TypeScript. ESM không tự đoán extension.

Cách 2: Dùng Trường exports trong package.json (Sạch và Có Khả Năng Mở Rộng)

Khi utils là một module boundary rõ ràng, hãy tạo package.json riêng cho nó với exports map:

// utils/package.json
{
  "name": "utils",
  "type": "module",
  "exports": {
    ".": "./index.js"
  }
}

Lúc này import ngắn ban đầu hoạt động trở lại:

import { formatDate } from './utils';

Node.js đọc utils/package.json, tìm thấy exports map, và resolve về utils/index.js. Không có gì kỳ diệu cả — chỉ là config tường minh.

Cách 3: Tạo File Barrel Re-export

Có hơn 10 file trong utils/? Tạo một file index.js re-export tất cả từ một nơi:

// utils/index.js
export { formatDate } from './formatDate.js';
export { slugify } from './slugify.js';
export { parseEnv } from './parseEnv.js';

Sau đó import tường minh từ file đó:

import { formatDate, slugify } from './utils/index.js';

API public gọn gàng, một câu import cho mỗi nơi sử dụng.

Cách 4: Người Dùng TypeScript — Đặt moduleResolution thành node16 hoặc bundler

Đang compile TypeScript sang ESM? tsconfig.json của bạn cần một chế độ resolution phù hợp với quy tắc ESM:

// tsconfig.json
{
  "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "node16",
    "target": "ES2022"
  }
}

Với node16 hoặc bundler, TypeScript sẽ báo lỗi directory import ngay lúc compile. Bạn bắt được lỗi trước khi node chạy — tốt hơn nhiều so với crash lúc runtime trên CI.

Tìm Tất Cả Import Lỗi Nhanh Chóng

Đừng sửa từng file một. Hãy chạy grep trên toàn bộ project trước:

# Tìm relative import trông như đường dẫn thư mục (không có extension)
grep -rn "from '\./[^']*[^.][^a-z]'" src/ --include="*.mjs" --include="*.js"

# Pattern rộng hơn — bắt cả nháy đơn lẫn nháy đôi
grep -rEn "from ['\"]\./[^'\"]+[^/]['\"]" src/

Bạn sẽ có danh sách đầy đủ kèm tên file và số dòng. Sửa tất cả trong một lần thay vì lần theo từng lỗi một.

Kiểm Tra Sau Khi Sửa

Chạy trực tiếp entry point của bạn:

node src/index.mjs

Hết lỗi rồi? Tốt. Để kiểm tra toàn bộ project, chạy test suite:

node --experimental-vm-modules node_modules/.bin/jest
# hoặc
npm test

Để kiểm tra module resolution riêng lẻ — không chạy logic ứng dụng:

node --input-type=module <<< "import './src/utils/index.js'; console.log('OK');"

Nếu in ra OK, resolution đã ổn.

Mẹo Tránh Lỗi Về Sau

  • Luôn viết extension .js trong ESM import — kể cả với file nguồn .ts. TypeScript compile chúng thành .js, đó là thứ Node.js thấy lúc runtime.
  • Bắt buộc bằng ESLint: eslint-plugin-import với "import/extensions": ["error", "always"] sẽ phát hiện extension bị thiếu trước khi code review.
  • Đang migrate codebase CJS lớn? Chạy codemod trước — cjs-to-esm-converter hoặc @babel/plugin-transform-modules-commonjs sẽ làm lộ ra tất cả các implicit resolution mà bạn đã phụ thuộc mà không hay biết.
  • Trong monorepo, hãy thêm trường exports vào package.json của mỗi sub-package. Tốn công hơn ban đầu, nhưng mỗi package sẽ có API boundary rõ ràng — lợi ích thấy rõ ngay khi có nhiều hơn hai nơi sử dụng.

Related Error Notes