ERR_MODULE_NOT_FOUND often appears after code worked in an editor, a test runner, or TypeScript. The important clue is that Node.js is resolving the JavaScript it actually runs. It does not automatically use every alias or extension rule understood by another tool.
Quick answer
Start with the path shown in the error, then check these causes in order:
- Add the runtime file extension to relative ESM imports.
- Confirm the file exists in the built output, not only in
src. - Check the package name and its
exportsrules. - Remove TypeScript path aliases unless the runtime resolves them too.
- Make sure letter casing matches the real filename.
For a local ESM file, this is the most common fix:
// Fails when Node runs ESM directly
import { formatUser } from './format-user';
// Node can resolve the emitted JavaScript file
import { formatUser } from './format-user.js';
read the missing URL literally
Node usually prints the exact module URL it tried to load:
Error [ERR_MODULE_NOT_FOUND]: Cannot find module
'/app/dist/format-user' imported from /app/dist/server.js
This tells you three useful facts: Node ran dist/server.js, looked inside dist, and searched for format-user without an extension. Check that location before changing package versions.
add extensions for relative ESM imports
When a project uses native ESM, relative imports normally need a full runtime filename:
{
"type": "module"
}
import { loadConfig } from './config.js';
In TypeScript source, .js can still be correct even when the source file is config.ts. With module and moduleResolution set to NodeNext, TypeScript understands that the emitted runtime file will be JavaScript.
inspect the build output
A correct import cannot load a file that was never emitted or copied.
src/
server.ts
templates/email.html
dist/
server.js
If server.js loads ./templates/email.html, your build must copy that template into dist/templates. TypeScript compiles code; it does not automatically copy every non-code asset.
do not assume aliases work in Node
This TypeScript setting helps the editor understand an alias:
{
"compilerOptions": {
"paths": {
"@app/*": ["./src/*"]
}
}
}
It does not rewrite the emitted import for Node. If the output still contains @app/users, Node needs a package import map, a loader, a bundler, or ordinary relative imports.
check package exports
If the missing module is inside a package, inspect the package’s documented entry points. A file may exist inside node_modules but remain intentionally private:
// May be blocked by the package exports map
import helper from 'some-package/internal/helper.js';
// Use a public entry point instead
import { helper } from 'some-package';
Reinstalling dependencies does not fix an import that the package never exported.
remember case-sensitive deployments
macOS may tolerate an import such as ./UserService.js when the file is named userService.js. Linux deployment environments usually do not. Match every character exactly and use one filename convention throughout the project.
a short debugging command
Run the same built entry file that production runs:
npm run build
node dist/server.js
Then follow the first missing path in the stack trace. Fixing the first resolution failure is more reliable than changing several module settings at once.
ERR_MODULE_NOT_FOUND is usually a disagreement between the import text and the runtime filesystem. Read the URL, inspect the emitted files, and make Node’s resolution rules explicit.