JavaScript ES Modules
Modern import and export syntax for browsers and bundlers. Modules are strict mode, defer by default in HTML, and share a single live binding per specifier.
Named and default exports
Each file can have multiple named exports and at most one default export.
Named exports
// math.js
export const PI = 3.14159;
export function sum(a, b) { return a + b; }
// or declare first
function diff(a, b) { return a - b; }
export { diff };
Importing named bindings
import { sum, PI } from './math.js';
import { sum as add, PI as pi } from './math.js';
import * as M from './math.js';
M.sum(1, 2);
Default export
// greet.js
export default function greet(name) {
return `Hello, ${name}`;
}
// importer chooses the local name
import greet from './greet.js';
import anyName from './greet.js';
Mixing default and named
// api.js
export default function fetchUser(id) { /* ... */ }
export function parseError(e) { /* ... */ }
import fetchUser, { parseError } from './api.js';
Paths: In browsers and many tools, include the .js extension in relative specifiers for predictable resolution. Bundlers may rewrite paths.
Re-exports
Aggregate public APIs from submodules without importing values into the current scope.
export { sum } from './math.js';
export { default as UserService } from './user-service.js';
export * from './constants.js';
export * as shapes from './shapes.js';
Barrel files (index.js)
A barrel re-exports many modules from one entry. Convenient for consumers, but can hurt tree-shaking and slow cold builds if everything re-exports transitively.
// components/index.js
export { Button } from './Button.js';
export { Modal } from './Modal.js';
Tip: Prefer direct imports for hot paths, or explicit named exports instead of export * when bundles grow large.
Dynamic import()
Returns a Promise for the module namespace. Enables code-splitting and conditional loading.
// Prefer a fixed map so the module path cannot be influenced by untrusted input
const locales = {
de: () => import('./locales/de.js'),
en: () => import('./locales/en.js'),
};
const tag = navigator.language.startsWith('de') ? 'de' : 'en';
const messages = await locales[tag]();
const { heavyFeature } = await import('./heavy-feature.js');
Security: Never build dynamic import() specifiers from URL params, post bodies, or other untrusted strings. That can become remote code execution (load unexpected modules) or path traversal depending on your bundler and server. Constrain to a whitelist or static paths.
Top-level await is allowed in ES modules (where supported) so await import(...) can appear at module scope.
import.meta
Host-specific metadata about the current module.
// URL of this module (native ESM in browser / Node)
new URL('./asset.png', import.meta.url);
// Vite: env flags injected at build time
if (import.meta.env.DEV) console.debug('dev build');
CommonJS and Node
Node supports both require (CJS) and import (ESM). Packages use "type": "module" in package.json or .mjs extensions for ESM.
// ESM importing a CJS default export often looks like:
import pkg from 'some-cjs-package';
// interop may synthesize default from module.exports
// Rare: load CJS from ESM in Node
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const legacy = require('./legacy.cjs');
Security: Treat require(...) paths like import paths: only resolve fixed or allow-listed modules. Never pass user-controlled strings into require or dynamic import.