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!