Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

JavaScript modules let you split a program into files with explicit dependencies: one module exports values, and another imports them. ES modules (ESM) are JavaScript’s standardized module format, but the browser, Node.js, or a bundler decides how an import path resolves. That distinction explains why the same-looking import can work in one environment and fail in another.

What is a JavaScript module?

A module is a JavaScript file that exposes selected values and can depend on values exposed by other modules. This makes dependencies visible in the code and lets you organize functionality into files rather than relying on one large script or implicit global variables.

ES modules use export to make a binding available to other modules and import to request it. The language specifies this syntax and its behavior; it does not prescribe a single universal way to turn every import path into a file. That resolution belongs to the execution host. TypeScript’s module theory describes this host-defined part of module behavior.

How do imports and exports work?

Named exports

A named export is imported by the name of the exported binding. In this example, both files use ESM syntax:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
// math.js
export function add(a, b) {
  return a + b;
}

// app.js
import { add } from './math.js';
console.log(add(2, 3));

add is a named export, and the braces in the import identify that name. The import refers to the exported binding; it is not a request to copy and rename the function arbitrarily.

Default exports

A module can also provide a default export. The importing module chooses the local name for that value:

// logger.js
export default function log(message) {
  console.log(message);
}

// app.js
import writeLog from './logger.js';
writeLog('Ready');

Named and default exports are different export forms, not a ranking of good and bad design. Named exports make the imported name correspond to an exported name; a default export gives the module one designated default value that the importer can name locally.

Static imports and dynamic imports

Static import declarations belong at the top level of a module. When loading should happen conditionally or asynchronously, use the import() expression instead:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
if (shouldLoadFeature) {
  const feature = await import('./feature.js');
  feature.run();
}

import() returns a promise, so it is suited to runtime decisions or asynchronous loading. It is not automatically a performance improvement; the result depends on the runtime and how any bundler builds the application.

Why import paths behave differently across environments

An import specifier is the text between the quotes, such as ./math.js or some-package. Its meaning depends on the host. Browsers, Node.js, and bundlers can use different resolution rules, even when the source uses the same ESM syntax.

  • Relative specifier: begins with a path such as ./ or ../.
  • Bare package specifier: names a package, such as some-package.
  • Absolute URL specifier: identifies a module by URL where the host supports that form.

For Node.js ESM, relative and absolute specifiers must include the file extension, and directory indexes must be fully specified. For example, write import './startup.js';, not import './startup'; or import './startup/';. This is a Node.js rule, not a universal rule for every bundler. Node’s ES module documentation also explains package resolution and the package.json exports field, which can restrict which package subpaths consumers may import.

How to use ES modules in Node.js

Node.js supports both ESM and CommonJS. Make the intended format clear with a file extension or package setting rather than assuming that every .js file has the same module format. The examples below target Node.js ESM.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Marker Format indicated Typical use
.mjs ESM Mark an individual file as ESM.
"type": "module" in package.json ESM for applicable .js files in that package scope Use ESM as the package’s default for JavaScript files.
.cjs CommonJS Mark an individual file as CommonJS.
"type": "commonjs" in package.json CommonJS for applicable .js files in that package scope Use CommonJS as the package’s default for JavaScript files.

Node.js also recognizes --input-type=module and --input-type=commonjs for input supplied through supported command-line forms. Its current documentation describes syntax detection when an explicit format marker is absent, but an explicit marker makes the intended format easier to see and avoids relying on detection. These format markers and detection behavior are documented in the Node.js ESM reference.

A minimal package setup

To use the earlier math.js and app.js example as Node.js ESM, put them in a package directory with:

{
  "type": "module"
}

Then run the entry file with node app.js. Keep the explicit .js extension in the relative import. If the package instead uses CommonJS, its files and import style need to follow CommonJS rules or use deliberate interoperation.

Importing CommonJS from Node.js ESM

When Node.js ESM imports a CommonJS module, the CommonJS module’s module.exports value is reliably available as the ESM default import:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import legacyLibrary from 'legacy-library';

Node.js may also infer named exports from some CommonJS code through static analysis, but this is best-effort. Patterns it cannot detect will not provide those names, and inferred named exports do not track later changes to the CommonJS exports object. Use the default import when you need the reliable mapping to module.exports.

Interop rules differ among Node.js, browsers, bundlers, transpilers, and TypeScript configurations. In particular, Node.js require() supports only synchronous ES modules; a module using top-level await cannot be loaded that way. See the Node.js documentation on ESM and CommonJS interoperation and TypeScript’s explanation of module interoperability.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Best practices for module design

  • Make dependencies explicit. Import values a file uses instead of relying on hidden globals.
  • Use export forms deliberately. Choose named exports when consumers should refer to a value by its exported name; choose a default export when the module has one designated default value. Neither form is inherently superior.
  • Write paths for the actual host. If code runs directly in Node.js ESM, include extensions on relative and absolute imports. Do not assume a bundler’s path behavior will work unchanged in Node.
  • Respect package boundaries. A package’s exports field defines supported public entry points; consumers should not assume every internal file path is importable.
  • Treat CommonJS named imports cautiously. Prefer the default import for Node.js CommonJS modules when you need predictable access to module.exports.
  • Align TypeScript with execution. Configure its module model and resolution behavior for the runtime or bundler that will actually process the code, rather than selecting settings only because they accept a particular import.

Choosing TypeScript module settings

TypeScript settings describe how the compiler should model module syntax, resolution, and output for the intended host; they do not erase the host’s runtime rules. A configuration that models a bundler may accept imports that do not work when emitted JavaScript runs directly in Node.js. TypeScript explains this distinction in its Modules Reference and Modules Theory.

  • For Node.js projects: the current TypeScript reference recommends node16, node18, or nodenext module modes. These model Node’s dual ESM/CommonJS system and choose behavior based on each file’s detected format.
  • For bundler-managed projects: use the documented bundler-oriented resolution mode when the bundler is responsible for processing imports. Choose the module setting based on whether the bundler processes the source directly or emitted JavaScript will run under Node.js.

nodenext does not mean “ESM only”: Node-oriented TypeScript modes can emit ESM or CommonJS according to file format. Check the TypeScript Modules Reference for the current options and their behavior.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.