ES6+ FeaturesFeatured
JavaScript Modules: Import, Export, and Module Systems
Master JavaScript modules with ES6 import/export syntax. Learn module patterns, dynamic imports, CommonJS vs ES modules, and best practices.
By JavaScriptDoc Team•
modulesimportexportes6javascript
JavaScript Modules: Import, Export, and Module Systems
Modules are a fundamental feature for organizing and structuring JavaScript applications. They allow you to split code into reusable pieces with clear dependencies and interfaces.
Introduction to Modules
Modules help solve several problems:
- Namespace pollution: Avoid global variables
- Dependency management: Clear import/export relationships
- Code organization: Logical separation of concerns
- Reusability: Share code across projects
// Before modules - global namespace pollution
var userManager = {
users: [],
addUser: function (user) {
/* ... */
},
};
// With modules - clean, isolated code
// userManager.js
export class UserManager {
constructor() {
this.users = [];
}
addUser(user) {
this.users.push(user);
}
}
// main.js
import { UserManager } from './userManager.js';
const manager = new UserManager();
ES6 Module Syntax
Named Exports
// math.js - Multiple named exports
export const PI = 3.14159;
export const E = 2.71828;
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
export class Calculator {
constructor() {
this.result = 0;
}
add(n) {
this.result += n;
return this;
}
}
// Alternative syntax - export list
const subtract = (a, b) => a - b;
const divide = (a, b) => a / b;
export { subtract, divide };
// Importing named exports
import { PI, add, Calculator } from './math.js';
import { subtract as minus } from './math.js'; // Rename import
import * as MathUtils from './math.js'; // Import all
console.log(PI); // 3.14159
console.log(add(2, 3)); // 5
console.log(MathUtils.multiply(4, 5)); // 20
Default Exports
// user.js - Default export
export default class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
greet() {
return `Hello, I'm ${this.name}`;
}
}
// logger.js - Default function
export default function log(message) {
console.log(`[${new Date().toISOString()}] ${message}`);
}
// config.js - Default object
export default {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3
};
// Importing default exports
import User from './user.js'; // Any name works
import log from './logger.js';
import config from './config.js';
const user = new User('John', 'john@example.com');
log(user.greet());
Mixed Exports
// utils.js - Mix of default and named exports
export default function formatDate(date) {
return date.toLocaleDateString();
}
export function formatTime(date) {
return date.toLocaleTimeString();
}
export function formatCurrency(amount) {
return `$${amount.toFixed(2)}`;
}
export const VERSION = '1.0.0';
// Import both default and named
import formatDate, { formatTime, formatCurrency, VERSION } from './utils.js';
// Or with different syntax
import { default as dateFormatter, formatTime, VERSION } from './utils.js';
Dynamic Imports
Dynamic imports allow loading modules on demand.
// Static import (top-level)
import { heavyFunction } from './heavy-module.js';
// Dynamic import (returns Promise)
async function loadHeavyModule() {
const module = await import('./heavy-module.js');
return module.heavyFunction();
}
// Conditional imports
async function loadTheme(themeName) {
const theme = await import(`./themes/${themeName}.js`);
return theme.default;
}
// Lazy loading on user action
button.addEventListener('click', async () => {
const { Modal } = await import('./components/Modal.js');
const modal = new Modal();
modal.show();
});
// Error handling
async function safeImport(modulePath) {
try {
const module = await import(modulePath);
return module;
} catch (error) {
console.error(`Failed to load module: ${modulePath}`, error);
return null;
}
}
Module Patterns
Barrel Exports
// components/index.js - Barrel file
export { Button } from './Button.js';
export { Modal } from './Modal.js';
export { Dropdown } from './Dropdown.js';
export { default as Form } from './Form.js';
// Clean imports from barrel
import { Button, Modal, Form } from './components/index.js';
// utils/index.js - Utility barrel
export * from './string-utils.js';
export * from './array-utils.js';
export * from './date-utils.js';
export { default as logger } from './logger.js';
Module Factory Pattern
// factory.js
export function createAPI(config) {
const baseURL = config.baseURL || 'https://api.example.com';
async function get(endpoint) {
const response = await fetch(`${baseURL}${endpoint}`);
return response.json();
}
async function post(endpoint, data) {
const response = await fetch(`${baseURL}${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
return response.json();
}
return { get, post };
}
// Usage
import { createAPI } from './factory.js';
const api = createAPI({ baseURL: 'https://myapi.com' });
const users = await api.get('/users');
Singleton Module
// database.js - Singleton pattern
class Database {
constructor() {
if (Database.instance) {
return Database.instance;
}
this.connection = null;
Database.instance = this;
}
connect(connectionString) {
this.connection = connectionString;
console.log('Connected to database');
}
query(sql) {
if (!this.connection) {
throw new Error('Not connected to database');
}
console.log(`Executing: ${sql}`);
}
}
export default new Database();
// Any import gets the same instance
import db from './database.js';
db.connect('mongodb://localhost:27017/myapp');
Module Organization
File Structure
// Feature-based organization
src/
├── features/
│ ├── auth/
│ │ ├── auth.service.js
│ │ ├── auth.controller.js
│ │ └── index.js
│ ├── users/
│ │ ├── user.model.js
│ │ ├── user.service.js
│ │ └── index.js
│ └── posts/
│ ├── post.model.js
│ ├── post.service.js
│ └── index.js
├── shared/
│ ├── utils/
│ ├── constants/
│ └── components/
└── index.js
// Layer-based organization
src/
├── models/
│ ├── user.model.js
│ └── post.model.js
├── services/
│ ├── auth.service.js
│ └── user.service.js
├── controllers/
│ ├── auth.controller.js
│ └── user.controller.js
└── index.js
Circular Dependencies
// Avoid circular dependencies
// Bad - Circular dependency
// a.js
import { b } from './b.js';
export const a = 'A' + b;
// b.js
import { a } from './a.js';
export const b = 'B' + a;
// Good - Break circular dependency
// constants.js
export const A_PREFIX = 'A';
export const B_PREFIX = 'B';
// a.js
import { B_PREFIX } from './constants.js';
export const a = A_PREFIX + B_PREFIX;
// b.js
import { A_PREFIX } from './constants.js';
export const b = B_PREFIX + A_PREFIX;
CommonJS vs ES Modules
CommonJS (Node.js)
// CommonJS exports
// math.js
const PI = 3.14159;
function add(a, b) {
return a + b;
}
module.exports = {
PI,
add,
};
// Or individual exports
exports.multiply = function (a, b) {
return a * b;
};
// CommonJS imports
const { PI, add } = require('./math.js');
const math = require('./math.js');
// Dynamic requires
const moduleName = './dynamic-module.js';
const dynamicModule = require(moduleName);
ES Modules in Node.js
// package.json
{
"type": "module" // Enable ES modules
}
// Or use .mjs extension
// utils.mjs
export function helper() {
return 'ES Module';
}
// Import CommonJS in ES Module
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const lodash = require('lodash');
// __dirname and __filename in ES Modules
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
Advanced Module Techniques
Re-exporting
// lib/index.js - Re-export from multiple modules
export { UserService } from './services/user.service.js';
export { AuthService } from './services/auth.service.js';
export { default as config } from './config.js';
// Re-export everything
export * from './utils.js';
export * as helpers from './helpers.js';
// Selective re-export with rename
export {
validateEmail as isValidEmail,
validatePhone as isValidPhone,
} from './validators.js';
Module Augmentation
// types.js - Base types
export interface User {
id: number;
name: string;
}
// extensions.js - Augment existing module
import { User } from './types.js';
// Extend the User interface
declare module './types.js' {
interface User {
email: string;
role: string;
}
}
// Module side effects
// polyfills.js
Array.prototype.customMethod = function() {
// Add method to Array prototype
};
// Import for side effects only
import './polyfills.js';
Conditional Exports
// package.json - Node.js conditional exports
{
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"browser": "./dist/browser.js"
},
"./utils": {
"import": "./dist/utils.mjs",
"require": "./dist/utils.cjs"
}
}
}
// Runtime conditional loading
const loadModule = async () => {
if (typeof window !== 'undefined') {
// Browser environment
return import('./browser-module.js');
} else {
// Node.js environment
return import('./node-module.js');
}
};
Module Best Practices
1. Clear Exports
// Good - Clear what's exported
export class UserManager {}
export function validateUser() {}
export const MAX_USERS = 100;
// Avoid - Unclear exports
export default {
UserManager: class {},
validateUser: function () {},
MAX_USERS: 100,
};
2. Consistent Module Structure
// user.service.js - Consistent structure
// 1. Imports
import { Database } from './database.js';
import { validateEmail } from './validators.js';
// 2. Constants
const MAX_LOGIN_ATTEMPTS = 3;
// 3. Main implementation
export class UserService {
constructor() {
this.db = new Database();
}
async createUser(userData) {
// Implementation
}
}
// 4. Helper functions
function hashPassword(password) {
// Implementation
}
// 5. Default export (if needed)
export default new UserService();
3. Dependency Management
// Good - Explicit dependencies
import { logger } from './logger.js';
import { config } from './config.js';
import { Database } from './database.js';
export class Service {
constructor() {
this.logger = logger;
this.config = config;
this.db = new Database(config.database);
}
}
// Avoid - Hidden dependencies
export class Service {
constructor() {
// Dependencies not clear from imports
this.data = globalData; // Bad!
}
}
4. Module Testing
// math.js
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
// math.test.js
import { add, multiply } from './math.js';
import { jest } from '@jest/globals';
describe('Math module', () => {
test('add function', () => {
expect(add(2, 3)).toBe(5);
});
test('multiply function', () => {
expect(multiply(3, 4)).toBe(12);
});
});
// Mock modules
jest.mock('./api.js', () => ({
fetchUser: jest.fn(() => Promise.resolve({ id: 1, name: 'Test' })),
}));
Performance Considerations
// Bundle size optimization
// Only import what you need
import { debounce } from 'lodash-es'; // Good
import _ from 'lodash'; // Bad - imports everything
// Tree shaking friendly exports
// Good - Named exports can be tree-shaken
export function utilA() {}
export function utilB() {}
// Bad - Default object export prevents tree shaking
export default {
utilA() {},
utilB() {},
};
// Lazy loading for performance
const routes = {
'/': () => import('./pages/Home.js'),
'/about': () => import('./pages/About.js'),
'/contact': () => import('./pages/Contact.js'),
};
async function navigateTo(path) {
const module = await routes[path]();
const Page = module.default;
renderPage(new Page());
}
Conclusion
JavaScript modules are essential for modern development:
- ES6 modules are the standard
- Import/Export syntax is clean and explicit
- Dynamic imports enable code splitting
- Module patterns help organize large applications
- CommonJS still relevant for Node.js
Key takeaways:
- Use named exports for utilities and libraries
- Use default exports for main module functionality
- Organize modules by feature or layer
- Avoid circular dependencies
- Leverage dynamic imports for performance
- Keep modules focused and single-purpose
Master modules to build scalable, maintainable JavaScript applications!