Photo by Yosuke Ota on Unsplash

On this page

1. Node.js ESM / CJS rules

File being imported Node module type Import syntax Extension required? Notes
.js ESM import ✅ Yes Relative ESM imports must include .js. Directory imports are not allowed.
.mjs ESM import ✅ Yes Explicit .mjs is required if you’re targeting ESM in Node.
.cjs CJS require ✅ Optional Node can resolve .cjs without extension. In ESM, you must use .cjs if you import.
.ts (source) N/A import (TS source) ❌ Do not use .ts at runtime TS compiler resolves .js specifiers back to .ts source. Runtime only sees .js.
.mts (TS ESM) ESM import ✅ Yes (use .js in runtime) Source file extension .mts is TypeScript-only; runtime uses .js.
.cts (TS CJS) CJS require ✅ Optional Same as above; TS source, runtime is .js.

Key rules by context

1. Relative imports in ESM (Node)

  • Always include explicit file extension: .js / .mjs / .cjs.
  • Example: import { helper } from "./utils/helper.js"; // ✅
  • Directory imports do not work: import { helper } from "./utils"; // ❌ ERR_UNSUPPORTED_DIR_IMPORT

2. Relative imports in CJS (Node)

  • .js or .cjs extensions are optional. Node will resolve: ./module.js ./module.cjs ./module/index.js ./module/index.cjs
  • Example: const helper = require("./utils"); // ✅ Node will find ./utils/index.js

3. TypeScript source files (.ts, .mts, .cts)

  • Always write runtime-compatible extensions (.js) in import specifiers, even if importing .ts. import { helper } from "./utils/helper.js"; // ✅ TS resolves this to ./utils/helper.ts
  • Node never sees .ts, .mts, .cts. These are compiled away.

4. Dynamic imports

  • Same rule: use the runtime extension (.js) in the specifier.
  • Example: const mod = await import("./feature.js");

2. When to use which extension

Import type Use in source Runtime seen by Node
.ts Never in import Node doesn’t see .ts
.mts Source file Compiled to .js at runtime
.cts Source file Compiled to .js at runtime
.js Runtime file Node sees .js
.cjs Runtime file Node sees .cjs
.mjs Runtime file Node sees .mjs

3. Summary guidelines

  • Always write the runtime extension in TypeScript source imports:

.js for .ts/.mts source targeting ESM

  • .cjs for .cts source targeting CJS
  • Never import .ts or .mts at runtime. Node cannot run them.
  • Directory imports only work in CJS, not in ESM. For ESM, always point to /index.js.
  • Dynamic imports follow the same rule: use runtime extension.
  • Use "exports" or "imports" in package.json to avoid writing /index.js in public APIs.

If you want, I can make a visual map showing all 6 file extensions, what TypeScript sees, and what Node sees, with arrows showing what extension to use in each import. It’s a really handy cheat sheet. Do you want me to do that?

    Search articles

    Type a word to search published articles.

    Open the search page