NodeJSES Modules (import / export)

ES Modules (import / export)

ES Modules (ESM) are the official JavaScript module system, standardized in ES2015 and supported natively by Node, browsers, and every modern tool. They use import/export syntax, load asynchronously, and offer features CommonJS cannot — static analysis, live bindings, and top-level await. For new projects, ESM is the recommended default.

Turning ESM on

Node treats a file as ESM when any of these is true:

Trigger

Scope

File ends in .mjs

That file only

"type": "module" in package.json

All .js files in that package

--input-type=module for --eval

The evaluated string

package.json — opt the whole project into ESM

JSON
{
  "name": "my-app",
  "type": "module"
}
Exporting

math.js

JS
// Named exports — as many as you like
export const add = (a, b) => a + b
export const PI = 3.14159

// Or declare first, export later
const multiply = (a, b) => a * b
export { multiply }

// One default export per module
export default function subtract(a, b) {
  return a - b
}
Importing

app.js

JS
import subtract, { add, PI } from './math.js'   // default + named
import * as math from './math.js'                // everything as a namespace
import { add as plus } from './math.js'          // rename on import

console.log(add(2, 3), subtract(5, 1), math.PI)
File extensions are required
Unlike CommonJS, relative ESM imports must include the extension: `import './utils.js'`, **not** `import './utils'`. Directory imports (relying on `index.js`) also do not work by default. This strictness is what lets ESM resolve without touching the disk to guess.
Imports are static and hoisted

import statements are not function calls — they are declarations the engine resolves before any code runs. This static structure is ESM's superpower (tools can tree-shake and analyze the dependency graph) but also its main constraint:

  • All imports are hoisted to the top and evaluated first, regardless of where you write them.

  • You cannot conditionally import with a static statement, or build the specifier from a runtime variable.

  • For dynamic needs, use the async import() function (next section).

JS
// ILLEGAL — static import cannot be conditional
// if (dev) import './tools.js'

// LEGAL — dynamic import() returns a promise
if (dev) {
  const tools = await import('./tools.js')
  tools.enable()
}
Live bindings — a real difference from CJS

ESM exports live bindings, not copies. An importer always sees the current value of an exported variable, even after the exporting module reassigns it — something CommonJS cannot do:

counter.js

JS
export let count = 0
export function increment() { count++ }

JS
import { count, increment } from './counter.js'
console.log(count)   // 0
increment()
console.log(count)   // 1 — the binding is live; no re-import needed
You still cannot reassign an import
Imported bindings are read-only *from the importer's side* — `count = 5` throws. Only the module that owns the binding can change it. This guarantees a single source of truth.
Top-level await

Because ESM loads asynchronously, you can await at the top level of a module — no async wrapper. Ideal for one-time setup:

JS
const res = await fetch('https://api.example.com/config')
export const config = await res.json()
It delays every importer
A module with top-level await does not finish loading until the await settles, and importers wait for it. Great for init; dangerous if the awaited work is slow or can hang — it stalls startup. See [async/await](/nodejs/async-await-refresher).
Interop with CommonJS

ESM can import CommonJS packages — the module's module.exports arrives as the default import. The reverse is harder: CommonJS cannot require() an ESM module (it is async); it must use dynamic import():

JS
// ESM importing a CommonJS package — exports become the default
import express from 'express'        // express is CJS; this works

// Named imports from CJS are best-effort (Node analyzes the exports)
import pkg from 'some-cjs-lib'
const { thing } = pkg                // safest if named import fails

The full comparison and migration guidance is in CommonJS vs ES Modules.

Next
Put the two systems side by side and learn when to use each in [CommonJS vs ES Modules](/nodejs/commonjs-vs-esm).