TypeScript

Fix Cannot find module defined in tsconfig paths at runtime

Why tsc compiles an @alias import cleanly and Node still crashes on it, and five runtime fixes tested on Node 22 with TypeScript 5.9.3 and 7.0.2.

Mahdi Ben RhoumaMahdi Ben Rhouma
8 min read

TL;DR#

paths in tsconfig.json only tells the TypeScript compiler where to find a module while it type checks. tsc writes your alias into the output unchanged, so node dist/src/index.js calls require() on the alias itself and Node throws Cannot find module. Adding include or moving baseUrl does not change that. Fix it at runtime: preload tsconfig-paths/register (for ts-node, or for compiled code with TS_NODE_BASEURL), rewrite the emitted imports with tsc-alias after tsc, run with tsx, or swap the aliases for package.json "imports" (#data/*), which Node resolves natively.

The error#

The Stack Overflow question sets up aliases for a mock server. Its tsconfig.json has "baseUrl": "./src" and maps @project/server/data/* to ../server/src/json/*, and the project fails with:

text
Error: Cannot find module '@project/server/data/accounts/accountsList'

The error was reproduced on 19 October 2026 in the official node:22-alpine image (Node v22.23.3) with TypeScript 5.9.3, on a project with the same folder layout. It keeps the question's baseUrl and paths, but raises target to es2019 and adds strict and outDir, because the question's ES5 target is rejected by the current compiler (more on that below). tsc exits with code 0. Running the output gives the error from the question (code frame and stack trimmed):

text
$ node dist/src/index.js
Error: Cannot find module '@project/server/data/accounts/accountsList'
Require stack:
- /work/dist/src/index.js
  code: 'MODULE_NOT_FOUND',

Why it happens#

The compiled file still contains the alias, word for word. Running grep -n require dist/src/index.js on the output shows lines 3 and 4 passing @project/server/data/accounts/accountsList and @project/app/modules/greet to require(), exactly as they were written in src/index.ts. Nothing was rewritten.

That is by design. The tsconfig reference for paths states: "Note that this feature does not change how import paths are emitted by tsc, so paths should only be used to inform TypeScript that another tool has this mapping and will use it at runtime or when bundling." The module resolution reference puts it more bluntly: it is "very easy to create path aliases that appear to work in TypeScript but will crash at runtime".

Node's CommonJS loader treats any bare specifier, one that does not start with ./, ../ or /, as a package name. As the tsconfig-paths README puts it, Node "will only look in the node_modules folders all the way up to the root of the filesystem". There is no such package, so it throws MODULE_NOT_FOUND. The baseUrl reference adds that the option "was designed for use in conjunction with AMD module loaders in the browser", and that since TypeScript 4.1 it is no longer needed for paths.

ts-node behaves the same way, and says so. Its README explains that "paths" are meant to describe mappings the runtime already performs, "For this reason, ts-node does not modify node's module resolution behavior to implement "paths" mappings." Running the source directly fails too:

text
$ npx ts-node src/index.ts
Error: Cannot find module '@project/server/data/accounts/accountsList'
Require stack:
- /work/src/index.ts

The include answer does not change the runtime#

The second-ranked answer moves baseUrl to ., rewrites both paths entries relative to the root and adds "include": ["./src", "./server"]. That is a tidier config, but it was tested as a negative control: tsc still exits 0, and node dist/src/index.js throws the identical Cannot find module '@project/server/data/accounts/accountsList'. Nothing in tsconfig.json can reach Node at run time.

Fix#

Pick the step that matches how you run the code. Each was run on its own against the failing project above.

  1. Running .ts files with ts-node: preload tsconfig-paths. tsconfig-paths reads paths from tsconfig.json and patches Node's module loading so the alias resolves to a real file.

    bash
    npm install -D tsconfig-paths
    npx ts-node -r tsconfig-paths/register src/index.ts

    Output: accounts loaded: 2. This ran with TypeScript 5.9.3 installed beside ts-node; with the 7.0.2 compiler that npm installs by default today, ts-node crashed before reaching your code (see the TypeScript 7 section below). To avoid typing the flag, put it in tsconfig.json, as the top answer and the ts-node README do. This variant also printed accounts loaded: 2 (excerpt; the rest of compilerOptions stays as it was):

    json
    {
      "ts-node": {
        "require": ["tsconfig-paths/register"]
      },
      "compilerOptions": {
        "baseUrl": "./src",
        "paths": {
          "@project/server/data/*": ["../server/src/json/*"]
        }
      }
    }
  2. Running compiled dist with tsconfig-paths: point it at dist. Preloading the hook in front of plain node is not enough on its own. The test run still threw Cannot find module, with tsconfig-paths/lib/register.js now in the stack, because the mapping leads to server/src/json/..., where only a .ts file exists. The tsconfig-paths README documents TS_NODE_BASEURL to override baseUrl. Point it at the compiled copy of src:

    bash
    TS_NODE_BASEURL=./dist/src node -r tsconfig-paths/register dist/src/index.js

    This printed accounts loaded: 2. Note that tsconfig-paths is now a runtime need, so it belongs in dependencies, not devDependencies, if the production install omits dev packages.

  3. Shipping plain JavaScript: rewrite the imports with tsc-alias after tsc. tsc-alias replaces each alias in the emitted files with a relative path, so the output needs no hook at all.

    bash
    npm install -D tsc-alias
    npx tsc && npx tsc-alias

    The compiled file afterwards:

    text
    3:const accountsList_1 = require("../server/src/json/accounts/accountsList.js");
    4:const greet_1 = require("./modules/greet.js");

    node dist/src/index.js then printed accounts loaded: 2. Wire both commands into the build script ("build": "tsc && tsc-alias") so a deploy never ships un-rewritten files.

  4. Running TypeScript directly without ts-node: use tsx. tsx resolved both aliases from tsconfig.json with no extra package: npx tsx src/index.ts printed accounts loaded: 2 (tsx v4.23.15, in a project on TypeScript 7.0.2). Steps 1, 2, 3 and 5 as written above ran on TypeScript 5.9.3; the TypeScript 7 section below covers 7.0.2.

  5. Removing the alias problem altogether: package.json "imports". The module resolution reference recommends package.json "imports" "as a standard replacement for convenience paths aliases". Node resolves # specifiers itself (subpath imports), and TypeScript resolves them when moduleResolution is node16, nodenext or bundler. For a local project it maps the dist targets back to the sources using outDir and rootDir; the reference adds that "using "imports" usually requires an explicit rootDir to be set", which is why rootDir appears below. The tested package.json:

    json
    {
      "name": "mock-server",
      "version": "1.0.0",
      "private": true,
      "imports": {
        "#data/*": "./dist/server/src/json/*.js",
        "#modules/*": "./dist/src/modules/*.js"
      }
    }

    And the tsconfig.json, with paths and baseUrl gone:

    json
    {
      "compilerOptions": {
        "module": "nodenext",
        "moduleResolution": "nodenext",
        "rootDir": ".",
        "outDir": "dist",
        "strict": true,
        "target": "es2022"
      },
      "include": ["src", "server"]
    }

    With the imports written as #data/accounts/accountsList and #modules/greet, tsc kept them as they are and node dist/src/index.js printed accounts loaded: 2. These targets point at dist, so this was tested on compiled output after a build only, not under ts-node or tsx. The same reference warns that published libraries must not rely on paths, and offers "imports" to libraries and apps alike.

The top Stack Overflow answer also suggests module-alias with a _moduleAliases block in package.json for the compiled build. That route was not part of this test; it duplicates every alias in a second file, which the options above avoid.

Verify the fix#

Two checks tell you whether the runtime, not just the compiler, knows the alias:

bash
grep -n require dist/src/index.js
node dist/src/index.js

If the require lines still show the alias, you need a hook at start-up (step 2). If they show relative paths (step 3) or # specifiers backed by package.json (step 5), plain node must run without one. A clean tsc exit proves nothing here: it was 0 in every failing run above.

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.

TypeScript 7 rejects the question's config before you reach Node#

On 19 October 2026 the latest tag of typescript on the npm registry points to 7.0.2, so a fresh npm install -D typescript gets that version. Run against the question's original tsconfig.json, npx tsc --noEmit stopped on the config itself:

text
tsconfig.json(3,9): error TS5102: Option 'baseUrl' has been removed. Please remove it from your configuration.
  Use '"paths": {"*": ["./src/*"]}' instead.
tsconfig.json(8,29): error TS5108: Option 'moduleResolution=node10' has been removed. Please remove it from your configuration.
tsconfig.json(13,40): error TS5090: Non-relative paths are not allowed. Did you forget a leading './'?
tsconfig.json(17,19): error TS5108: Option 'target=ES5' has been removed. Please remove it from your configuration.

The third line follows from the first: without baseUrl, each paths target is resolved relative to tsconfig.json and must be written as a relative path. Only line 13, "modules/*", was flagged; the "../server/src/json/*" target on the next line was accepted. The corrected config drops baseUrl and writes every target relative to tsconfig.json:

json
{
  "compilerOptions": {
    "module": "nodenext",
    "rootDir": ".",
    "outDir": "dist",
    "strict": true,
    "target": "es2022",
    "paths": {
      "@project/app/modules/*": ["./src/modules/*"],
      "@project/server/data/*": ["./server/src/json/*"]
    }
  },
  "include": ["src", "server"]
}

TypeScript 7.0.2 compiled it with exit 0, and the output then failed at run time with the same Cannot find module '@project/server/data/accounts/accountsList'. The compiler changed; the fact that paths never reaches the emitted code did not.

The runtime fixes were then run again in that TypeScript 7.0.2 project:

  • tsc-alias (step 3): rewrote both requires to the same relative paths as on 5.9.3, and node dist/src/index.js printed accounts loaded: 2.
  • tsx (step 4): npx tsx src/index.ts printed accounts loaded: 2.
  • package.json "imports" (step 5, same package.json and tsconfig.json as above): printed accounts loaded: 2.
  • tsconfig-paths on dist (step 2) needs a different override here. With no baseUrl in tsconfig.json, the paths targets start from the project root, so TS_NODE_BASEURL=./dist/src still threw Cannot find module, while TS_NODE_BASEURL=./dist node -r tsconfig-paths/register dist/src/index.js printed accounts loaded: 2. In short, the override must point to the folder inside dist that mirrors the directory your paths are relative to.

One more trap on 7.0.2: ts-node 10.9.2 did not start at all in that project. It crashed in its own config loader with TypeError: Cannot read properties of undefined (reading 'fileExists'), before any module was resolved, and the installed typescript/lib folder held only five files, among them tsc.js. The ts-node route (step 1) was tested only with TypeScript 5.9.3 installed explicitly (npm install -D typescript@5.9.3); on 7.0.2, use tsx or one of the compile-then-run fixes above.

Where the same alias breaks next#

The same "the compiler knows, the tool does not" pattern returns in other runners. Create React App refuses paths outright, covered in compilerOptions.paths must not be set; Next.js reads paths itself while Jest and ESLint need their own wiring, as explained in tsconfig paths not working in Next.js. If ts-node's stack now shows return old(m, filename) with ERR_REQUIRE_ESM, the alias is solved and an ESM-only dependency is the new problem: see ERR_REQUIRE_ESM in ts-node. The dist/src/index.js path in the output above comes from the same compiler layout that makes node dist/main miss its file in a container, which Cannot find module '/dist/main' in a Docker build untangles with rootDir.

If you move the project to "type": "module" while switching to nodenext, the error can change from Cannot find module to ERR_MODULE_NOT_FOUND for relative imports written without .js; continue with ERR_MODULE_NOT_FOUND: relative import missing .js. Whatever runner you settle on, the rule from the TypeScript docs holds: paths describes a mapping that something else must perform, so pick the something else on purpose and check the emitted require lines once.

Further reading: Fix shadcn CLI wrong component paths with extended tsconfig.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote