How require() Resolves Modules
When you write require('foo'), Node has to turn that short string into one specific file on disk. The algorithm it uses — module resolution — looks like magic until you know the rules, and then a whole class of "Cannot find module" errors becomes obvious. This page walks the algorithm step by step.
Three kinds of specifier
The first character of the string decides which branch Node takes:
Specifier | Treated as | Resolution strategy |
|---|---|---|
| Core module | Returned immediately from the binary |
| Relative path | Resolved against the current file |
| Absolute path | Used as-is |
| Package (bare specifier) | Searched up the |
Core modules win first
Built-in modules (fs, path, http…) are checked before anything touches the filesystem, so they always resolve instantly. The node: prefix makes this explicit and unspoofable — require('node:fs') can never accidentally load a node_modules/fs:
require('node:path') // always the built-in, recommended
require('path') // also the built-in, unless... nothing shadows coreRelative and absolute paths
For ./, ../, or / specifiers, Node tries a sequence of candidates in order, adding extensions and looking for directory entry points:
require('./utils') tries, in order
1. ./utils (exact file, if it has an extension) 2. ./utils.js 3. ./utils.json 4. ./utils.node (compiled C++ addon) 5. ./utils/package.json → its "main" field 6. ./utils/index.js (the directory's default entry)
The node_modules walk
For a bare specifier like require('express'), Node walks up the directory tree, checking a node_modules folder at each level until it finds a match or hits the filesystem root:
From /app/src/api/route.js, require('express') checks
/app/src/api/node_modules/express /app/src/node_modules/express /app/node_modules/express ← found here /node_modules/express
This upward walk is why a dependency installed once at the project root is visible to every nested file, and why two packages can each have their own copy of a shared dependency at different depths.
Resolving inside a package
Once Node finds node_modules/express, it must pick the entry file. Modern packages use the "exports" field (which can also restrict what is importable); older ones use "main"; the fallback is index.js:
A package's package.json
{
"name": "express",
"main": "./index.js",
"exports": {
".": "./index.js",
"./package.json": "./package.json"
}
}Inspecting resolution yourself
require.resolve() runs the full algorithm but returns the path instead of loading the module — invaluable for debugging "which copy am I actually getting?":
console.log(require.resolve('express'))
// /app/node_modules/express/index.js
console.log(require.resolve('./utils'))
// /app/src/utils.js/app/node_modules/express/index.js /app/src/utils.js
Debugging "Cannot find module"
Bare specifier not found → the package is not installed; run
npm installand check it is inpackage.json.Relative path not found → typo in the path, wrong extension, or missing
index.jsin a folder.ERR_PACKAGE_PATH_NOT_EXPORTED→ you imported a path the package"exports"does not expose.Works locally, fails in prod → case-sensitivity:
./Utilsvs./utilsdiffers on Linux but not macOS/Windows.