ES Modules
ES Modules (ESM) are the official JavaScript module system. They replace the old CommonJS require() syntax used in older Node.js code.
Named vs Default Exports
math.js
// Named exports — can have many per file
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
export const PI = 3.14159;
// You can also export at the bottom
const multiply = (a, b) => a * b;
const divide = (a, b) => a / b;
export { multiply, divide };
user.js
// Default export — one per file, represents the "main thing" the module exports
export default class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
}
Importing
// Named imports — must match exact export names
import { add, subtract, PI } from './math.js';
// Rename on import
import { add as sum } from './math.js';
// Import all named exports as a namespace
import * as MathUtils from './math.js';
MathUtils.add(1, 2);
MathUtils.PI;
// Default import — you choose the name
import User from './user.js';
import MyUser from './user.js'; // same export, different local name
// Mix default and named
import User, { validateEmail } from './user.js';
Re-exporting (Barrel Files)
Barrel files (index.js) are a common pattern to simplify imports:
utils/index.js
// Re-export everything from sub-modules
export { add, subtract, PI } from './math.js';
export { formatDate, parseDate } from './date.js';
export { slugify, truncate } from './string.js';
export { default as User } from './user.js';
app.js
// Now you can import from one place
import { add, formatDate, User } from './utils/index.js';
// instead of:
// import { add } from './utils/math.js';
// import { formatDate } from './utils/date.js';
// import User from './utils/user.js';
Dynamic Imports
Load modules on demand (code splitting, lazy loading):
// Static import — runs at module evaluation time
import { heavyLib } from './heavy.js'; // always loaded
// Dynamic import — returns a Promise, loaded on demand
const loadChart = async () => {
const { default: Chart } = await import('./chart.js');
return new Chart(data);
};
// Load based on condition
if (isAdminUser) {
const { AdminPanel } = await import('./admin.js');
}
// In the browser — triggered on user action
button.addEventListener('click', async () => {
const { openModal } = await import('./modal.js');
openModal();
});
ESM vs CommonJS
You'll see both in the wild. Know the difference:
// CommonJS (old Node.js, .js or .cjs files)
const path = require('path');
const { readFile } = require('fs');
module.exports = { myFunction };
module.exports.default = MyClass;
// ES Modules (modern, .mjs or .js with "type": "module" in package.json)
import path from 'path';
import { readFile } from 'fs';
export { myFunction };
export default MyClass;
package.json
{
"type": "module" // Treats all .js files as ESM
}
Use ESM
All new projects should use ESM. TypeScript compiles to ESM by default. Vite, Next.js, and NestJS all use ESM.
Module Organization Patterns
src/
index.ts # entry point
lib/
auth.ts
db.ts
index.ts # barrel — re-exports lib/*
utils/
format.ts
validate.ts
index.ts # barrel — re-exports utils/*
types/
index.ts # shared TypeScript types
src/utils/format.ts
export function formatCurrency(amount: number, currency = 'USD'): string {
return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount);
}
export function formatDate(date: Date): string {
return new Intl.DateTimeFormat('en-US').format(date);
}
src/utils/index.ts
export * from './format';
export * from './validate';
anywhere in your app
import { formatCurrency, formatDate } from '../utils';