Skip to content

ES Modules in Node.js

ES Modules (ESM) are the standard JavaScript module system — the same import/export syntax you use in React and Vue. Node.js supports them natively, and TypeScript with "module": "NodeNext" uses them throughout this course.

src/utils/math.ts
export function add(a: number, b: number): number {
return a + b
}
export function subtract(a: number, b: number): number {
return a - b
}
export default function multiply(a: number, b: number): number {
return a * b
}
src/index.ts
import multiply, { add, subtract } from './utils/math.js'
console.log(add(2, 3)) // 5
console.log(subtract(10, 4)) // 6
console.log(multiply(3, 4)) // 12

Note the .js extension in the import path — even though the source file is .ts, the compiled output will be .js. TypeScript with "moduleResolution": "NodeNext" requires explicit extensions.

There are two ways:

1. Add "type": "module" to package.json — all .js files in the project use ESM:

{
"type": "module"
}

2. Use .mjs extension — only files with .mjs use ESM.

When TypeScript compiles with "module": "NodeNext", it outputs standard ES module syntax that Node.js understands natively.

FeatureCommonJSES Modules
Syntaxrequire() / module.exportsimport / export
LoadingSynchronousAsynchronous
__dirnameAvailableNot available (use import.meta.url)
Tree shakingNoYes (bundlers can eliminate unused exports)
Top-level awaitNoYes

CommonJS’s __dirname isn’t available in ES Modules. Use import.meta.url instead:

import { fileURLToPath } from 'url'
import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)

In practice, you’ll rarely need this — the Bulletin API uses path.join(process.cwd(), ...) when building paths relative to the project root.

// Named exports — use curly braces when importing
export function foo() { }
export const bar = 42
import { foo, bar } from './module.js'
// Default export — no curly braces, any name works
export default function main() { }
import main from './module.js'
import doThing from './module.js' // same as above, different name

Barrel files (index.ts) can re-export from multiple files:

src/utils/index.ts
export { add, subtract } from './math.js'
export { formatDate } from './date.js'
// Cleaner imports
import { add, formatDate } from './utils/index.js'
  1. Convert your math.ts utility to use named exports (export function).
  2. Add a default export for the most commonly used function.
  3. Import both named and default exports in index.ts and verify they work.
  4. Try removing the .js extension from the import path — observe the TypeScript error.
  • ES Modules use import/export — the standard JavaScript module system.
  • Enable in Node.js with "type": "module" in package.json.
  • TypeScript with "moduleResolution": "NodeNext" requires explicit .js extensions in import paths.
  • __dirname isn’t available in ESM — use import.meta.url or process.cwd().
  • Named exports use {} in imports; default exports don’t.