Moduleshigh

Module Resolution Algorithm

When you call require('./foo') or import './foo', Node follows a resolution algorithm to find the actual file: it checks the exact path, then adds extensions (.js, .json, .node), then looks for an index file, and finally searches node_modules directories up the tree.

Memory anchor

Module resolution is like a GPS that tries multiple routes: exact address first, then nearby streets (.js, .json), then the neighborhood index.js, then drives up the directory tree checking every node_modules town until it reaches the root. The 'exports' field in package.json is a bouncer list—unlisted paths get rejected at the door.

Expected depth

For bare specifiers (require('lodash')), Node searches node_modules in the current directory, then parent directories, up to the filesystem root. The package.json exports field (added in Node.js 12) allows packages to define their public API surface and map subpath imports, taking precedence over direct file path access. The main field in package.json specifies the entry point for CJS; for ESM, the exports field with a '.' key is checked first.

Deep — senior internals

The resolution algorithm for ESM bare specifiers is stricter than CJS: you cannot require('lodash/array') if lodash's package.json exports field doesn't expose that path. This breaks existing CJS patterns. The exports field supports conditions (import, require, node, browser, development, production) allowing packages to serve different implementations per environment. The imports field in package.json allows internal package imports with '#' prefix, providing subpath aliasing without exposing them externally. Node.js module resolution hooks (--experimental-loader or register() in Node.js 20+) enable custom resolution for transpilers, mocking, and module federation.

🎤Interview-ready answer

Node resolves require()/import in order: (1) core modules, (2) relative/absolute paths with extension fallback (.js/.json/.node) and index.js fallback, (3) node_modules traversal up to root. The package.json exports field controls ESM and conditional CJS/ESM resolution, superseding the main field. Bare specifiers without a match in exports will throw ERR_PACKAGE_PATH_NOT_EXPORTED.

Common trap

Adding a package.json exports field with only specific subpaths breaks all other deep imports of that package. Library authors who add exports without including all previously public paths cause semver-major breakage for consumers who were using undocumented deep imports—a common source of ecosystem pain when packages add ESM support.

Related concepts