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.
import and export
Section titled “import and export”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}import multiply, { add, subtract } from './utils/math.js'
console.log(add(2, 3)) // 5console.log(subtract(10, 4)) // 6console.log(multiply(3, 4)) // 12Note 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.
Enabling ESM in Node.js
Section titled “Enabling ESM in Node.js”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.
Key differences from CommonJS
Section titled “Key differences from CommonJS”| Feature | CommonJS | ES Modules |
|---|---|---|
| Syntax | require() / module.exports | import / export |
| Loading | Synchronous | Asynchronous |
__dirname | Available | Not available (use import.meta.url) |
| Tree shaking | No | Yes (bundlers can eliminate unused exports) |
| Top-level await | No | Yes |
__dirname in ESM
Section titled “__dirname in ESM”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 vs default exports
Section titled “Named vs default exports”// Named exports — use curly braces when importingexport function foo() { }export const bar = 42
import { foo, bar } from './module.js'
// Default export — no curly braces, any name worksexport default function main() { }
import main from './module.js'import doThing from './module.js' // same as above, different nameRe-exports
Section titled “Re-exports”Barrel files (index.ts) can re-export from multiple files:
export { add, subtract } from './math.js'export { formatDate } from './date.js'// Cleaner importsimport { add, formatDate } from './utils/index.js'Exercise
Section titled “Exercise”- Convert your
math.tsutility to use named exports (export function). - Add a default export for the most commonly used function.
- Import both named and default exports in
index.tsand verify they work. - Try removing the
.jsextension 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"inpackage.json. - TypeScript with
"moduleResolution": "NodeNext"requires explicit.jsextensions in import paths. __dirnameisn’t available in ESM — useimport.meta.urlorprocess.cwd().- Named exports use
{}in imports; default exports don’t.