File extensions for module imports

1. Node.js ESM / CJS rules

File being importedNode module typeImport syntaxExtension required?Notes
.jsESMimport✅ YesRelative ESM imports must include .js. Directory imports are not allowed.
.mjsESMimport✅ YesExplicit .mjs is required if you’re targeting ESM in Node.
.cjsCJSrequire✅ OptionalNode can resolve .cjs without extension. In ESM, you must use .cjs if you import.
.ts (source)N/Aimport (TS source)❌ Do not use .ts at runtimeTS compiler resolves .js specifiers back to .ts source. Runtime only sees .js.
.mts (TS ESM)ESMimport✅ Yes (use .js in runtime)Source file extension .mts is TypeScript-only; runtime uses .js.
.cts (TS CJS)CJSrequire✅ OptionalSame 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 typeUse in sourceRuntime seen by Node
.tsNever in importNode doesn’t see .ts
.mtsSource fileCompiled to .js at runtime
.ctsSource fileCompiled to .js at runtime
.jsRuntime fileNode sees .js
.cjsRuntime fileNode sees .cjs
.mjsRuntime fileNode sees .mjs

3. Summary guidelines

  1. Always write the runtime extension in TypeScript source imports:
    • .js for .ts/.mts source targeting ESM
    • .cjs for .cts source targeting CJS
  2. Never import .ts or .mts at runtime. Node cannot run them.
  3. Directory imports only work in CJS, not in ESM. For ESM, always point to /index.js.
  4. Dynamic imports follow the same rule: use runtime extension.
  5. 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?

About Author

Mathias Bothe To my job profile

I am Mathias from Heidelberg, Germany. I am a passionate IT freelancer with 15+ years experience in programming, especially in developing web based applications for companies that range from small startups to the big players out there. I create Bosycom and initiated several software projects.