Fix ERR_UNSUPPORTED_DIR_IMPORT When Importing a Directory in Node.js ES Modules

intermediate๐Ÿ’š Node.js2026-05-17| Node.js 12+ with ES Modules (ESM), any OS (Linux, macOS, Windows). Triggered when using .mjs files or "type": "module" in 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

The Error

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?

Classic migration pain. Something that worked perfectly in CommonJS now blows up the moment you switch to ES Modules. Or you added "type": "module" to your package.json, hit save, and watched half your imports break at once.

Why This Happens

CommonJS was forgiving. You could write require('./utils') and Node.js would silently resolve it to ./utils/index.js. That behavior is gone in ESM โ€” the spec requires fully explicit specifiers. Node.js will not guess the file for you.

So this import:

import { formatDate } from './utils';

...throws ERR_UNSUPPORTED_DIR_IMPORT because ./utils is a directory. Node.js sees a directory, refuses to pick an entry point, and stops dead.

Step-by-Step Fix

Option 1: Add the Explicit File Path (Quickest Fix)

Point the import at the actual file:

// Before (broken in ESM)
import { formatDate } from './utils';

// After (fixed)
import { formatDate } from './utils/index.js';

The .js extension is required โ€” even for TypeScript-compiled output. ESM does zero extension guessing.

Option 2: Use the exports Field in package.json (Clean, Scalable)

When utils is a well-defined module boundary, give it its own package.json with an exports map:

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

Now the original short import works again:

import { formatDate } from './utils';

Node.js reads utils/package.json, finds the exports map, and resolves to utils/index.js. No magic โ€” just explicit config.

Option 3: Create a Barrel Re-export File

Got 10+ files under utils/? Create an index.js that re-exports everything from one place:

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

Then import from it explicitly:

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

Clean public API, one import statement per consumer.

Option 4: TypeScript Users โ€” Set moduleResolution to node16 or bundler

Compiling TypeScript to ESM? Your tsconfig.json needs a resolution mode that matches ESM rules:

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

With node16 or bundler, TypeScript flags directory imports at compile time. You catch the error before node does โ€” much better than a runtime crash in CI.

Find All Broken Imports Fast

Don't fix files one by one. Run grep across the whole project first:

# Find relative imports that look like directory paths (no extension)
grep -rn "from '\./[^']*[^.][^a-z]'" src/ --include="*.mjs" --include="*.js"

# Broader pattern โ€” catches both single and double quotes
grep -rEn "from ['\"]\./[^'\"]+[^/]['\"]" src/

You'll get a full list with file and line numbers. Fix them all in one pass instead of chasing errors one at a time.

Verify the Fix

Run your entry point directly:

node src/index.mjs

Error gone? Good. For a full project check, run the test suite:

node --experimental-vm-modules node_modules/.bin/jest
# or
npm test

To validate module resolution in isolation โ€” without running any app logic:

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

If it prints OK, resolution is clean.

Tips to Avoid This Going Forward

  • Always write .js extensions in ESM imports โ€” even for .ts source files. TypeScript compiles them to .js, so that's what Node.js sees at runtime.
  • Enforce it with ESLint: eslint-plugin-import with "import/extensions": ["error", "always"] catches missing extensions before code review.
  • Migrating a large CJS codebase? Run a codemod first โ€” cjs-to-esm-converter or @babel/plugin-transform-modules-commonjs will surface all the implicit resolutions you depended on without realizing it.
  • In monorepos, add an exports field to every sub-package's package.json. It's extra upfront work, but it gives each package a clean, explicit API boundary โ€” which pays off the moment you have more than two consumers.

Related Error Notes