ES Modules are singletons by default - they're only executed once:
// database.js
const connection = await connectToDatabase();
export { connection };
// Multiple imports get the same instance
import { connection } from './database.js'; // Same connection
// features.js
export const ENABLE_DARK_MODE = true;
export const ENABLE_ANALYTICS = false;
// app.js
import { ENABLE_ANALYTICS } from './features.js';
if (ENABLE_ANALYTICS) {
await import('./analytics.js');
}
// plugin-manager.js
const plugins = [];
export function registerPlugin(plugin) {
plugins.push(plugin);
}
export function getPlugins() {
return plugins;
}
// plugins/logger.js
import { registerPlugin } from '../plugin-manager.js';
registerPlugin({ name: 'logger', init() { /* ... */ } });
// router.js
const routes = {
'/': () => import('./pages/home.js'),
'/about': () => import('./pages/about.js'),
'/contact': () => import('./pages/contact.js')
};
async function navigate(path) {
const module = await routes[path]();
module.render();
}
Create a barrel file to simplify imports:
// utils/index.js
export { add, subtract } from './math.js';
export { formatDate } from './date.js';
export { validateEmail } from './validation.js';
// Now consumers can import everything from one place
import { add, formatDate } from './utils/index.js';
Performance Warning:
Barrel files can hurt tree-shaking in some bundlers. If a barrel re-exports a large module, importing any single export may pull in the entire barrel. Consider direct imports for performance-critical code paths.
Use module factories to inject dependencies, making code testable and configurable:
// logger.js - injectable module factory
export function createLogger(transport = console) {
return {
info: (msg) => transport.log(`[INFO] ${msg}`),
error: (msg) => transport.error(`[ERROR] ${msg}`)
};
}
// app.js - production usage
import { createLogger } from './logger.js';
const logger = createLogger();
// test.js - inject mock transport
import { createLogger } from './logger.js';
const mockTransport = { log: vi.fn(), error: vi.fn() };
const logger = createLogger(mockTransport);
Load different module implementations based on the runtime environment:
// Using dynamic imports for environment branching
const db = await import(
process.env.NODE_ENV === 'test'
? './db-mock.js'
: './db-real.js'
);
// Using package.json conditional exports
// package.json
{
"exports": {
".": {
"node": "./src/platform-node.js",
"browser": "./src/platform-browser.js",
"default": "./src/platform-fallback.js"
}
}
}