TypeScript

Fix: Directory import is not supported resolving ES modules

Why Node throws ERR_UNSUPPORTED_DIR_IMPORT under "type": "module", three fixes reproduced on Node 24, and why the old specifier flag no longer helps.

Mahdi Ben RhoumaMahdi Ben Rhouma
7 min read

TL;DR#

Under "type": "module" (or in a .mjs file), Node.js resolves relative and absolute import paths the way a browser does: it loads the exact file you name and never looks inside a folder for an index.js. import database from './database' therefore fails with ERR_UNSUPPORTED_DIR_IMPORT. Write the full path, ./database/index.js, or give the folder a short name in the package.json "imports" map (#database) and import that. The --experimental-specifier-resolution=node flag from older answers was removed in Node.js 19.0.0: on current releases it changes nothing.

The error#

The Stack Overflow question behind this page has an app.js that imports ./database, a database/index.js next to it, and "type": "module" in package.json. The same layout, rebuilt in a throwaway node:24-bookworm-slim container on 11 October 2026, fails with:

text
Error [ERR_UNSUPPORTED_DIR_IMPORT]: Directory import '/w/src/database' is not supported resolving ES modules imported from /w/src/app.js
  code: 'ERR_UNSUPPORTED_DIR_IMPORT',
  url: 'file:///w/src/database'
}
 
Node.js v24.21.0

Node.js 18.20.8 and 22.23.3 print the same first line for the same files. The first path is the folder Node.js refused to load, and the second is the file that contains the faulty import. That second path is what tells you where to look, especially when it points inside node_modules (see the last section).

Why it happens#

Node.js has two module loaders with two resolution algorithms. CommonJS require('./database') tries ./database.js, then ./database/index.js, and so on. The ES module loader does none of that. The ESM documentation is explicit:

A file extension must be provided when using the import keyword to resolve relative or absolute specifiers. Directory indexes (e.g. './startup/index.js') must also be fully specified.

The specifier ./database therefore resolves to the URL file:///w/src/database. Node.js finds a directory at that URL, not a file, and throws the error that the errors reference describes as "import a directory URL is unsupported". If nothing at all existed at that URL (a file imported without its .js extension, for instance), the same check in Node's resolver source would throw ERR_MODULE_NOT_FOUND instead. That extensionless-file case has its own page: ERR_MODULE_NOT_FOUND for a relative import without .js.

As a control, the same database/index.js folder loads through require('./database') in a CommonJS package on Node.js 24.21.0. The folder layout is not the problem. The folder lookup is something only require() does, so an index-file layout breaks as soon as the importing file is loaded as an ES module. Switching a package to "type": "module" has a second effect, also seen in the reproduction on Node.js 24.21.0: a .js file in that package that still calls require() stops with require is not defined in ES module scope. That is the per-file CommonJS-or-ESM decision covered in exports is not defined in ES module scope, where keeping such files as .cjs is one of the fixes.

Check your own error message#

Paste the full error, including the imported from line, into the explainer below. It picks out the folder Node.js refused and gives the matching fixes: name the index file, or import through the package name and an "exports" subpath. It runs in your browser and nothing is sent.

Runs entirely in your browser — nothing you paste is sent anywhere. Tokens and passwords that appear in paths or URLs are masked in the output.

Optional: package.json, file extension, Node.js version (sharper advice)
Try:

Fix#

1. Name the index file#

This is the fix from the accepted answer, and it changes one line:

js
// src/app.js
import database from './database/index.js';
 
await database();

Run it again:

bash
node src/app.js
text
database: connect() called

Apply the same rule to every relative import in the project. Folders need /index.js, and files need their extension (./config.js, not ./config). This output assumes database/index.js declares its function properly. With the file exactly as posted in the question, the next run fails with a ReferenceError instead, which is fixed further down.

2. Or give the folder a short name in "imports"#

If you prefer a short specifier, declare it once in package.json. The packages documentation says entries in this field must start with #, and they only apply to imports made from inside the package itself:

json
{
  "name": "node-starter",
  "version": "1.0.0",
  "type": "module",
  "imports": {
    "#database": "./src/database/index.js"
  }
}
js
// src/app.js
import database from '#database';
 
await database();

Node.js 24.21.0 loads it and prints database: connect() called. The map points at a file, so there is still no directory lookup. It is just written once instead of in every import.

3. Or self-reference the package through "exports"#

The errors reference recommends a third route: list a subpath in "exports" and import the package by its own name. With "exports": { ".": "./src/app.js", "./database": "./src/database/index.js" } in the same package.json, import database from 'node-starter/database' also loads on Node.js 24.21.0. Use this when the folder is part of what the package offers to other code. For something internal, "imports" keeps it private.

Verify the fix#

Run the entry file directly, with no flag:

bash
node src/app.js

If it now fails with a different error, read the second path in the message again. It may point at a deeper file that has the same kind of import. Fix them one at a time until the program starts.

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.

The question's own code fails next#

Once the path is fixed, the database/index.js from the question fails on its first line:

text
export default connect = async () => {
                       ^
 
ReferenceError: connect is not defined

export default connect = … assigns to a variable that was never declared, and modules always run in strict mode, where that assignment throws a ReferenceError. Declare the function instead:

js
// src/database/index.js
export default async function connect() {
  try {
    console.log('database: connect() called');
  } catch (error) {}
}

With that change, the reproduction prints database: connect() called. In real code, replace the empty catch with error handling. A swallowed connection error only resurfaces later as a confusing failure.

The flag from older answers no longer exists#

The second-highest answer recommends node --experimental-specifier-resolution=node app.js. On Node.js 18.20.8 this still works. It prints ExperimentalWarning: The Node.js specifier resolution flag is experimental and loads the folder. The flag was removed in Node.js 19.0.0 (changelog, PR #44859). What makes it a trap is how current releases react. On Node.js 22.23.3 and 24.21.0 the command still starts without any warning about the flag, then fails with the exact same ERR_UNSUPPORTED_DIR_IMPORT. If a script or a Dockerfile still carries the flag, it does nothing. Remove it and fix the imports.

TypeScript compiles the bad import under one setting only#

In a TypeScript project, the compiler setting decides whether you see the problem at build time or at run time. With "module": "nodenext", TypeScript 6.0.3 rejects the folder import before anything runs:

text
src/app.ts(1,22): error TS2834: Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Consider adding an extension to the import path.

The fix is the same one, written with the .js extension of the output file, even in a .ts source:

ts
// src/app.ts
import database from './database/index.js';
 
await database();
json
{
  "compilerOptions": {
    "module": "nodenext",
    "target": "es2022",
    "rootDir": "src",
    "outDir": "dist",
    "strict": true
  },
  "include": ["src"]
}

The "rootDir" line matters on TypeScript 6: without it, tsc stops with TS5011 before checking the imports. With "module": "esnext" and "moduleResolution": "bundler", the same extensionless import compiled with no error on TypeScript 6.0.3. Running the emitted dist/app.js with plain Node.js then throws Directory import '/t/dist/database' is not supported. If the output runs on Node.js without a bundler, nodenext is the setting that catches every folder import at compile time. If the project runs through ts-node and the error is about require() rather than a folder, that is a different problem: see ERR_REQUIRE_ESM in ts-node.

When the path points inside node_modules#

If the folder in the message sits in node_modules, the faulty import is in a published package, not in your code. In next-auth issue #9202, a release of @auth/core imported lib/actions from lib/index.js as a folder. Projects that built it with Vite then failed with this exact message. The maintainer closed it as fixed via PR #9205 and asked users to upgrade. Do the same: upgrade the package, or pin the release before the one that broke. Editing files in node_modules will not survive the next install.

The other case is your own deep import of a package folder. Mongoose 9.11.1 has no "exports" field, so its internal folders can be reached by path, and import driver from 'mongoose/lib/drivers/node-mongodb-native' fails on Node.js 24.21.0 with the same error plus a hint:

text
Error [ERR_UNSUPPORTED_DIR_IMPORT]: Directory import '/m/node_modules/mongoose/lib/drivers/node-mongodb-native' is not supported resolving ES modules imported from /m/src/dep.js
Did you mean to import "mongoose/lib/drivers/node-mongodb-native/index.js"?

Importing the suggested …/node-mongodb-native/index.js path loads the module. The hint did not appear for the relative ./database import on the same Node.js release, so do not count on it. When a package does publish an "exports" map and that map leaves the folder out, the error you get is a different one, ERR_PACKAGE_PATH_NOT_EXPORTED. Import an entry the map lists, as explained in ERR_PACKAGE_PATH_NOT_EXPORTED: subpath not in exports.

Folder imports are not the only import rule that differs from require(). A JSON file loaded with import needs an import attribute, covered in ERR_IMPORT_ASSERTION_TYPE_MISSING when importing JSON. For the folder import itself, the durable fix is the boring one: write the index file you mean, and let "imports" shorten it if the paths get long.

Further reading: Fix Day.js: dayjs_1.default is not a function. Also see Fix "X is not a constructor" from a plain require(). Also see Fix Babel "native ECMAScript module configuration file". Also see Import a directory of files in a loop: TS1343, require().

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote