Master the two main JavaScript module systems - ESM (ECMAScript Modules) and CommonJS - to write better, more maintainable code. 📦
Understanding Module Systems 🎯
JavaScript modules help organize code into reusable, encapsulated units. Let's explore both ESM and CommonJS approaches.
ESM (ECMAScript Modules)
Basic Export/Import
// math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
// main.js
import { add, subtract } from './math.js';
console.log(add(5, 3)); // 8
Default Exports
// user.js
export default class User {
constructor(name) {
this.name = name;
}
}
// main.js
import User from './user.js';
const user = new User('John');
Mixed Exports
// api.js
export const BASE_URL = 'https://api.example.com';
export default class API {
constructor() {
this.baseUrl = BASE_URL;
}
}
// main.js
import API, { BASE_URL } from './api.js';
CommonJS Modules
Basic Exports
// config.js
module.exports = {
apiKey: 'abc123',
baseUrl: 'https://api.example.com'
};
// app.js
const config = require('./config.js');
console.log(config.apiKey);
Individual Exports
// utils.js
exports.formatDate = (date) => {
return date.toISOString();
};
exports.formatCurrency = (amount) => {
return `${amount.toFixed(2)}`;
};
// main.js
const { formatDate, formatCurrency } = require('./utils.js');
Key Differences 🔄
1. Syntax
// ESM
import { something } from './module.js';
export const thing = 42;
// CommonJS
const something = require('./module.js');
module.exports.thing = 42;
2. Loading Behavior
// ESM - Static imports (hoisted)
import { readFile } from 'fs/promises';
// CommonJS - Dynamic requires
const fs = process.env.NODE_ENV === 'test'
? require('fs-mock')
: require('fs');
3. Asynchronous vs Synchronous
// ESM - Asynchronous
import('./module.js').then(module => {
module.doSomething();
});
// CommonJS - Synchronous
const module = require('./module.js');
module.doSomething();
Modern Features 🌟
1. Dynamic Imports
async function loadModule() {
if (condition) {
const module = await import('./feature.js');
return module.default;
}
}
2. Module Namespaces
// utils.js
export const helper1 = () => {};
export const helper2 = () => {};
// main.js
import * as Utils from './utils.js';
Utils.helper1();
3. Re-exporting
// index.js
export { default as User } from './user.js';
export { default as Post } from './post.js';
export * from './utils.js';
Real-World Patterns 💡
1. Barrel Exports
// components/index.js
export { default as Button } from './Button.js';
export { default as Input } from './Input.js';
export { default as Form } from './Form.js';
// app.js
import { Button, Input, Form } from './components';
2. Lazy Loading
const AdminPanel = {
async render() {
const { default: Admin } = await import('./admin.js');
return new Admin();
}
};
3. Module Configuration
// config.js
export default {
development: {
api: 'http://localhost:3000'
},
production: {
api: 'https://api.example.com'
}
}[process.env.NODE_ENV || 'development'];
// app.js
import config from './config.js';
Best Practices 📝
1. Clear Module Boundaries
// ✅ Good - Clear responsibility
export class UserService {
async getUser(id) {}
async updateUser(id, data) {}
}
// ❌ Bad - Mixed responsibilities
export class Service {
async getUser(id) {}
async getPosts() {}
async getComments() {}
}
2. Explicit Exports
// ✅ Good - Clear what's being exported
export { User, createUser, updateUser };
// ❌ Bad - Unclear exports
export * from './user.js';
3. Consistent Import Style
// ✅ Good - Consistent style
import React, { useState, useEffect } from 'react';
import { Button, Input } from './components';
// ❌ Bad - Mixed styles
import React from 'react';
const { useState } = React;
import Button from './components/Button';
Performance Optimization 🚀
1. Tree Shaking
// utils.js
export const unused = () => {};
export const used = () => {};
// main.js
import { used } from './utils.js';
// unused will be removed in production build
2. Code Splitting
// Route-based code splitting
const routes = {
home: () => import('./pages/home.js'),
about: () => import('./pages/about.js'),
contact: () => import('./pages/contact.js')
};
3. Preload Hints
<link rel="modulepreload" href="./heavy-module.js">
Module Bundling
1. Webpack Configuration
// webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader'
}
]
}
};
2. Rollup Configuration
// rollup.config.js
export default {
input: 'src/index.js',
output: {
file: 'bundle.js',
format: 'esm'
}
};
Migration Strategies 🔄
1. Gradual Migration
// Legacy CommonJS module
const oldModule = require('./old-module.js');
// New ESM module
import { newFeature } from './new-module.js';
// Mixing both during migration
export const combined = {
...oldModule,
newFeature
};
2. Compatibility Layer
// compatibility.js
import moduleESM from './module.esm.js';
module.exports = moduleESM;
Additional Resources
Understanding both ESM and CommonJS module systems is crucial for modern JavaScript development. Each has its strengths and use cases, and knowing when to use each one will help you write better, more maintainable code.