NodeJSHow require() Resolves Modules

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

node:fs, fs

Core module

Returned immediately from the binary

./utils, ../lib/x

Relative path

Resolved against the current file

/abs/path

Absolute path

Used as-is

express, lodash/fp

Package (bare specifier)

Searched up the node_modules chain

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:

JS
require('node:path')   // always the built-in, recommended
require('path')        // also the built-in, unless... nothing shadows core
Relative 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

Text
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)
Extension-less imports are a CJS convenience
`require('./utils')` happily finds `utils.js`. ES Modules are stricter — they generally require the full filename including `.js`. This difference trips people migrating CJS code to ESM; see [CommonJS vs ES Modules](/nodejs/commonjs-vs-esm).
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

Text
/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

JSON
{
  "name": "express",
  "main": "./index.js",
  "exports": {
    ".": "./index.js",
    "./package.json": "./package.json"
  }
}
"exports" can block deep imports
If a package defines `"exports"`, paths *not* listed there are forbidden — `require('express/lib/router')` throws `ERR_PACKAGE_PATH_NOT_EXPORTED` even though the file exists. This is intentional encapsulation by the package author; use only the documented public entry points.
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?":

JS
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 install and check it is in package.json.

  • Relative path not found → typo in the path, wrong extension, or missing index.js in a folder.

  • ERR_PACKAGE_PATH_NOT_EXPORTED → you imported a path the package "exports" does not expose.

  • Works locally, fails in prod → case-sensitivity: ./Utils vs ./utils differs on Linux but not macOS/Windows.

Next
See the invisible function Node wraps every module in — the source of `require`, `module`, and `__dirname` — in [The Module Wrapper Function](/nodejs/module-wrapper).