Cannot Find Module Error in Node.js: Complete Fix Guide for Backend Developers
The Error: Cannot find module '...' message is one of the most common (and most misunderstood) Node.js runtime failures. It can mean anything from “you forgot to install a dependency” to “your build output is missing files” to “your package export map blocks deep imports” to “your runtime is resolving modules differently than you think.”
This guide explains exactly how Node.js resolves modules, why the error happens, and provides practical, copy‑pasteable commands to diagnose and fix it in real backend projects (CommonJS, ESM, TypeScript, monorepos, Docker, CI, and serverless).
Table of Contents
- What the error really means
- Read the error like a backend engineer
- How Node.js resolves modules (CommonJS vs ESM)
- Fast triage checklist (90 seconds)
- Fixes by root cause
- 1) Dependency not installed or not in the right place
- 2) Wrong import path (relative vs absolute)
- 3) File extension and ESM rules
- 4)
type: "module"and mixed module systems - 5) Package
exportsblocks deep imports - 6) TypeScript builds: compiled output missing modules
- 7) Path aliases (
tsconfig paths) not resolved at runtime - 8) Monorepos and workspaces resolution issues
- 9) Docker/CI: node_modules not present in the runtime image
- 10) Case sensitivity and cross-platform bugs
- 11) Global vs local installs confusion
- 12) Corrupted install / lockfile mismatch
- Debugging tools and commands
- Preventing the error in production
- Quick reference: common scenarios and fixes
What the error really means
When Node.js throws:
Error: Cannot find module 'some-module'
it means the module loader attempted to resolve an import/require request and failed.
That request can be:
- A package name (e.g.,
express,@aws-sdk/client-s3) - A relative path (e.g.,
./utils/logger.js) - An absolute path (rare in Node without custom loaders)
- A subpath import (e.g.,
lodash/get,date-fns/format) - A built-in module (rare, but possible if you typo it:
fs/promisesvsfs/promise)
The loader looks in specific places in a specific order. If it can’t find a matching file or package entry point, you get the error.
Read the error like a backend engineer
A typical stack trace looks like this:
node:internal/modules/cjs/loader:1078
throw err;
Error: Cannot find module 'jsonwebtoken'
Require stack:
- /app/src/auth/verifyToken.js
- /app/src/routes/private.js
- /app/src/server.js
Key information:
- The missing request:
'jsonwebtoken' - The resolution mode: the trace shows
cjs/loader→ CommonJSrequire()is in play - Require stack: the chain of files that led to the failing require
For ESM, you might see:
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'dotenv' imported from /app/src/index.js
or:
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/app/src/utils/logger' imported from /app/src/index.js
ESM errors often include the importing file and sometimes require file extensions.
How Node.js resolves modules (CommonJS vs ESM)
Understanding resolution prevents guesswork.
CommonJS (require())
When you do:
const pkg = require("some-package");
Node roughly does:
- If it’s a core module like
fs, load it. - If it starts with
./,../, or/, treat it as a path and try:- exact file
- file with extensions (
.js,.json,.node) - directory with
package.json"main"orindex.js
- Otherwise treat it as a package:
- walk up directories looking for
node_modules/some-package - read
some-package/package.json - use
"main"(or default toindex.js)
- walk up directories looking for
ESM (import)
With:
import pkg from "some-package";
Node uses ESM resolution rules:
- Still searches
node_modules, but respects:package.json"exports"(very important)package.json"type": "module"(affects.jsinterpretation)
- For relative imports, Node ESM typically requires explicit file extensions:
import "./logger.js"notimport "./logger"
This is a major cause of “Cannot find module” after migrating from CommonJS to ESM.
Fast triage checklist (90 seconds)
Run these in your project root:
-
Confirm you’re in the correct directory
pwd ls cat package.json -
Check Node version
node -v -
Check if the dependency exists
npm ls jsonwebtoken # or pnpm why jsonwebtoken # or yarn why jsonwebtoken -
Check if node_modules exists
ls -la node_modules | head -
Try resolving the module from Node directly
node -p "require.resolve('jsonwebtoken')"For ESM packages you can still often use
require.resolveas a diagnostic, but be aware it follows CJS semantics. -
Search for the import
rg "jsonwebtoken" -n # or grep -R "jsonwebtoken" -n src
If these steps don’t immediately reveal the issue, move to the root-cause sections.
Fixes by root cause
1) Dependency not installed or not in the right place
Symptoms
Cannot find module 'express'npm ls expressshows(empty)or errorsnode_modules/expressis missing
Fix
Install it in the project root (where package.json lives):
npm install express
# or
pnpm add express
# or
yarn add express
If it should be a dev dependency:
npm install -D nodemon
pnpm add -D nodemon
yarn add -D nodemon
Common pitfall: installing in the wrong directory
If you ran npm install in a subfolder, you might have node_modules there, but your runtime starts elsewhere. Confirm:
node -p "process.cwd()"
Your process’s current working directory affects resolution in some tooling setups (though Node’s module resolution is based on the importing file’s location, not cwd, but tooling and scripts often assume a root).
2) Wrong import path (relative vs absolute)
Symptoms
Cannot find module './util/logger'- You renamed/moved files
- Works in one environment but not another
Fix
Verify the file exists:
ls -la src/util
If your file is src/utils/logger.js, but you import ./util/logger, fix the path.
Also check relative path direction:
From src/routes/private.js:
// correct if logger is at src/utils/logger.js
const logger = require("../utils/logger");
A fast way to validate a relative path is to print the resolved absolute path:
console.log(require("path").resolve(__dirname, "../utils/logger"));
3) File extension and ESM rules
Symptoms
- You use
importsyntax - Error says it cannot find
.../logger(without extension) - Project uses
"type": "module"or.mjs
Example problem
// ESM
import { logger } from "./logger";
In Node ESM, this often fails unless you include the extension:
import { logger } from "./logger.js";
Fix
-
Confirm you are in ESM mode:
cat package.json | sed -n '1,120p'Look for:
{ "type": "module" } -
Update relative imports to include extensions:
./file.js../dir/index.js
-
If you compile TypeScript to ESM, ensure the emitted JS includes correct extensions (see the TypeScript section).
4) type: "module" and mixed module systems
Symptoms
- You switched to ESM and now CommonJS requires break
- Errors like:
ReferenceError: require is not defined in ES module scope- or
Cannot find moduledue to wrong entry points
Fix options
Option A: Stay CommonJS
- Remove
"type": "module"frompackage.json - Use
require()andmodule.exports - Use
.cjsfor CommonJS if you need mixed files
Option B: Go full ESM
- Use
import/export - Replace CommonJS-only patterns
- If you must import a CommonJS module, you can often do:
or:import pkg from "some-cjs-package";import { createRequire } from "node:module"; const require = createRequire(import.meta.url); const pkg = require("some-cjs-package");
Option C: Mixed with explicit extensions
- Use
.cjsfor CJS files and.mjsfor ESM files to avoid ambiguity.
5) Package exports blocks deep imports
Modern packages often define an "exports" map in package.json to control what paths consumers may import.
Symptoms
- You import a subpath like:
import merge from "lodash/merge"; - It fails with module not found (or
ERR_PACKAGE_PATH_NOT_EXPORTED)
Diagnose
Inspect the package’s package.json:
cat node_modules/some-package/package.json | sed -n '1,200p'
Look for:
"exports": {
".": "./dist/index.js"
}
If only "." is exported, deep imports like some-package/internal/file.js are not allowed.
Fix
- Import from the public entry:
import { merge } from "some-package"; - Or use the documented subpath exports (if provided):
import thing from "some-package/thing"; - If you control the package, update
"exports"to include needed paths.
6) TypeScript builds: compiled output missing modules
A very common backend issue: your code compiles, but at runtime Node cannot find compiled files.
Symptoms
- You run
node dist/index.js - Error:
Cannot find module './routes/userRoutes' - In
dist/, the file doesn’t exist or the path differs
Diagnose
Check your build output:
rm -rf dist
npm run build
find dist -maxdepth 3 -type f | head -n 50
Check tsconfig.json:
cat tsconfig.json
Key fields:
"rootDir": where TS sources are"outDir": where JS output goes"include"/"exclude": what gets compiled
Fix patterns
A) Ensure all source files are included
If routes are in src/routes, but your include only covers src/index.ts, they won’t compile.
Example tsconfig.json:
{
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"module": "CommonJS",
"target": "ES2020"
},
"include": ["src/**/*.ts"]
}
B) Copy non-TS files
If you import JSON files or load templates, they may not be copied to dist.
Options:
- Use a copy step:
mkdir -p dist/views cp -R src/views dist/views - Or configure your build tool (tsup, esbuild, webpack) to include assets.
7) Path aliases (tsconfig paths) not resolved at runtime
TypeScript can compile imports like @/utils/logger, but Node doesn’t understand that alias by default.
Symptoms
- Works in IDE
tsccompiles (sometimes)- Runtime fails:
Cannot find module '@/utils/logger'
Fix options
Option A: Use relative imports Most robust for Node without bundling:
import { logger } from "../utils/logger";
Option B: Use a runtime resolver
If you run TS directly (e.g., ts-node), use tsconfig-paths:
npm i -D ts-node tsconfig-paths
Run:
node -r ts-node/register -r tsconfig-paths/register src/index.ts
Option C: Compile and rewrite paths
Use a tool like tsc-alias:
npm i -D tsc-alias
Build script:
{
"scripts": {
"build": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json"
}
}
Option D: Use Node’s imports (ESM)
For ESM projects, you can use package.json "imports" with # specifiers, but it requires deliberate design and consistent usage.
8) Monorepos and workspaces resolution issues
Workspaces (npm, pnpm, Yarn) change where dependencies are installed (often hoisted to the repo root). This can confuse runtime if you execute from the wrong package or deploy only part of the repo.
Symptoms
- Works locally in monorepo
- Fails in production deployment of a single package
node_modulesstructure differs from local
Diagnose
Check workspace config:
cat package.json
# look for "workspaces"
List workspace packages (npm example):
npm -ws ls
Check where a dependency is installed:
npm ls some-dep --all
Fix
- Ensure each deployable package lists its own runtime dependencies in its
package.json. - Install dependencies in the correct workspace:
npm install express -w packages/api
# pnpm:
pnpm add express --filter ./packages/api
# yarn:
yarn workspace @your-scope/api add express
- If deploying a single workspace, run install in that workspace context or use workspace-aware install in CI.
9) Docker/CI: node_modules not present in the runtime image
A classic: everything works locally, but the container crashes with Cannot find module.
Symptoms
- Docker build succeeds
- Container starts and immediately fails
- You use multi-stage builds but forgot to copy dependencies
Diagnose inside container
Build and run a shell:
docker build -t my-api .
docker run --rm -it my-api sh
ls -la
ls -la node_modules | head
node -p "require.resolve('express')"
Fix: correct Dockerfile patterns
Example: Node backend with production deps
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
CMD ["node", "src/server.js"]
If you build TypeScript
FROM node:20-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src ./src
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/index.js"]
Key idea: the runtime image must contain:
dist/(or your JS output)node_modules/with production dependencies- any runtime assets
10) Case sensitivity and cross-platform bugs
macOS and Windows often use case-insensitive file systems; Linux typically is case-sensitive. That means an import can work locally and fail in production.
Symptoms
- Works on macOS, fails on Linux
- Error mentions a path that looks correct except for capitalization
Example:
require("./UserService"); // but file is userservice.js or userService.js
Fix
- Match the exact filename case.
- Enforce via linting and CI:
- ESLint rules
- TypeScript
forceConsistentCasingInFileNames: true
Check Git for case-only renames (Git can be tricky on case-insensitive FS). You may need:
git mv UserService.js _UserService.js
git mv _UserService.js userService.js
11) Global vs local installs confusion
If you installed a package globally, your project still can’t import it unless it’s installed locally (or you intentionally configure global resolution, which is not recommended for backend services).
Symptoms
npm i -g typescriptdonenoderuntime says cannot findtypescript(or other package)
Fix
Install locally:
npm i -D typescript
Then use npx (or pnpm dlx) for binaries:
npx tsc -v
12) Corrupted install / lockfile mismatch
Sometimes node_modules is in a broken state (especially after switching Node versions, package managers, or merging lockfiles).
Symptoms
- Random missing modules
npm lsshows invalid or unmet dependencies- CI fails but local works (or vice versa)
Fix: clean reinstall
npm
rm -rf node_modules package-lock.json
npm cache verify
npm install
pnpm
rm -rf node_modules pnpm-lock.yaml
pnpm store prune
pnpm install
yarn
rm -rf node_modules yarn.lock
yarn cache clean
yarn install
If you’re in CI, prefer deterministic installs:
npm cipnpm install --frozen-lockfileyarn install --frozen-lockfile
Debugging tools and commands
When the cause isn’t obvious, use the following to see what Node is doing.
1) Print resolution result
CommonJS:
node -p "require.resolve('express')"
node -p "require.resolve('./src/server.js')"
If it fails, Node will throw—use that as confirmation.
2) Show Node’s search paths (CommonJS)
node -p "module.paths"
From a specific directory:
node -e "process.chdir('src'); console.log(module.paths)"
3) Trace module loading (CommonJS)
Node has diagnostic flags. A practical one is:
node --trace-warnings src/server.js
For deeper debugging, you can instrument resolution:
// debug-resolve.cjs
const Module = require("module");
const original = Module._resolveFilename;
Module._resolveFilename = function (request, parent, isMain, options) {
console.log("Resolving:", request, "from", parent && parent.filename);
return original.call(this, request, parent, isMain, options);
};
require("./src/server");
Run:
node debug-resolve.cjs
This is extremely effective in monorepos and Docker where paths differ.
4) Check what file actually imports the missing module
Use ripgrep:
rg "from 'dotenv'|require\\('dotenv'\\)" -n src
Or find all imports of a package:
rg "from ['\"]@aws-sdk" -n src
5) Validate main / exports of a package
node -p "require('some-package/package.json').main"
node -p "require('some-package/package.json').exports"
If "exports" exists, deep imports may be blocked.
Preventing the error in production
1) Use deterministic installs in CI
Use lockfiles and CI-friendly commands:
npm ci
# or
pnpm install --frozen-lockfile
# or
yarn install --frozen-lockfile
2) Add a startup “dependency sanity check” (optional)
For critical services, you can validate key modules at boot:
// sanity.cjs
const required = ["express", "pg", "dotenv"];
for (const name of required) {
try {
require.resolve(name);
} catch (e) {
console.error(`Missing dependency: ${name}`);
process.exit(1);
}
}
console.log("Sanity check OK");
Run before starting:
node sanity.cjs && node src/server.js
3) Enforce consistent casing (TypeScript)
In tsconfig.json:
{
"compilerOptions": {
"forceConsistentCasingInFileNames": true
}
}
4) Avoid runtime path aliases unless you bundle
Aliases are fine, but only if you:
- bundle your app (esbuild/webpack/tsup), or
- rewrite aliases at build time, or
- use a runtime resolver intentionally
For plain Node services, relative imports are the least surprising.
5) Align local and production Node versions
Use .nvmrc:
node -v > .nvmrc
Or specify engines in package.json:
{
"engines": {
"node": ">=20"
}
}
In CI, actually enforce it by using the same Node version.
Quick reference: common scenarios and fixes
Scenario A: “Cannot find module ‘express’”
- Fix: install it
npm i express
Scenario B: “Cannot find module ’./routes’” after TypeScript build
- Fix: confirm
dist/routesexists; ensureincludecovers routes; rebuildrm -rf dist && npm run build
Scenario C: ESM error for extensionless import
- Fix:
import "./routes.js";
Scenario D: Works locally, fails in Docker
- Fix: ensure runtime image has
node_modulesand build output; use multi-stage properlydocker run --rm -it my-api sh -lc "ls -la node_modules | head"
Scenario E: Deep import blocked by exports
- Fix: import from package public API; check
exportscat node_modules/pkg/package.json | grep -n "\"exports\"" -n
Scenario F: Monorepo workspace package missing dependency in deployment
- Fix: add dependency to the correct workspace
npm i pg -w packages/api
Final mental model
When you see Cannot find module, don’t treat it as “Node is broken.” Treat it as:
- What exactly is being requested? (package name vs relative path vs subpath)
- Which module system is being used? (CJS vs ESM)
- From which file is it imported? (require stack / imported from)
- Does the file/package exist in the runtime environment? (local vs Docker vs CI)
- Are exports/aliases/build steps changing the expected paths?
If you want, paste the exact error message + your package.json (and tsconfig.json if applicable) and I can point to the most likely root cause and the minimal fix.