← Back to Tutorials

Cannot Find Module Error in Node.js: Complete Fix Guide for Backend Developers

node.jsbackend developmentjavascripttypescriptdebuggingmodule resolutionesmcommonjsnpmyarn

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

  1. What the error really means
  2. Read the error like a backend engineer
  3. How Node.js resolves modules (CommonJS vs ESM)
  4. Fast triage checklist (90 seconds)
  5. Fixes by root cause
  6. Debugging tools and commands
  7. Preventing the error in production
  8. 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:

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:

  1. The missing request: 'jsonwebtoken'
  2. The resolution mode: the trace shows cjs/loader → CommonJS require() is in play
  3. 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:

  1. If it’s a core module like fs, load it.
  2. 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" or index.js
  3. Otherwise treat it as a package:
    • walk up directories looking for node_modules/some-package
    • read some-package/package.json
    • use "main" (or default to index.js)

ESM (import)

With:

import pkg from "some-package";

Node uses ESM resolution rules:

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:

  1. Confirm you’re in the correct directory

    pwd
    ls
    cat package.json
  2. Check Node version

    node -v
  3. Check if the dependency exists

    npm ls jsonwebtoken
    # or
    pnpm why jsonwebtoken
    # or
    yarn why jsonwebtoken
  4. Check if node_modules exists

    ls -la node_modules | head
  5. Try resolving the module from Node directly

    node -p "require.resolve('jsonwebtoken')"

    For ESM packages you can still often use require.resolve as a diagnostic, but be aware it follows CJS semantics.

  6. 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

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

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

Example problem

// ESM
import { logger } from "./logger";

In Node ESM, this often fails unless you include the extension:

import { logger } from "./logger.js";

Fix

  1. Confirm you are in ESM mode:

    cat package.json | sed -n '1,120p'

    Look for:

    { "type": "module" }
  2. Update relative imports to include extensions:

    • ./file.js
    • ../dir/index.js
  3. 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

Fix options

Option A: Stay CommonJS

Option B: Go full ESM

Option C: Mixed with explicit extensions


5) Package exports blocks deep imports

Modern packages often define an "exports" map in package.json to control what paths consumers may import.

Symptoms

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


6) TypeScript builds: compiled output missing modules

A very common backend issue: your code compiles, but at runtime Node cannot find compiled files.

Symptoms

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:

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:


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

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

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

npm install express -w packages/api
# pnpm:
pnpm add express --filter ./packages/api
# yarn:
yarn workspace @your-scope/api add express

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

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:


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

Example:

require("./UserService"); // but file is userservice.js or userService.js

Fix

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

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

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:


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:

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’”

Scenario B: “Cannot find module ’./routes’” after TypeScript build

Scenario C: ESM error for extensionless import

Scenario D: Works locally, fails in Docker

Scenario E: Deep import blocked by exports

Scenario F: Monorepo workspace package missing dependency in deployment


Final mental model

When you see Cannot find module, don’t treat it as “Node is broken.” Treat it as:

  1. What exactly is being requested? (package name vs relative path vs subpath)
  2. Which module system is being used? (CJS vs ESM)
  3. From which file is it imported? (require stack / imported from)
  4. Does the file/package exist in the runtime environment? (local vs Docker vs CI)
  5. 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.