DEV Community

Ansh Sheladiya
Ansh Sheladiya

Posted on

JavaScript Modules Explained: ES Modules, Imports, Exports, and Best Practices

JavaScript modules provide a clean way to split a large application into smaller, focused files. Instead of keeping utilities, business logic, configuration, and application code in one place, modules let each part of the system have a clear responsibility.

Modern JavaScript uses ES Modules as the standard approach for sharing functionality between files. Once you understand named exports, default exports, imports, and module boundaries, building maintainable Node.js and browser applications becomes much easier.

Understanding JavaScript Modules

A module is simply a JavaScript file whose variables and functions are scoped to that module unless they are explicitly exported. This prevents accidental global variables and makes dependencies visible directly in the code. A module can export multiple named values or one default value, depending on how its API is designed.

Named exports are useful when a module provides several related utilities, such as formatting functions or validation helpers. Default exports work well when a module has one primary responsibility, such as a service or class. Consumers then use import statements to explicitly declare which functionality they need.

In Node.js, ES Modules can be enabled by using a .mjs file extension or by setting the package.json type to module. The same module syntax is also supported directly by modern browsers when scripts use type="module". Modules are loaded once and their exported bindings can be reused wherever they are imported.

A practical module structure often separates concerns into files such as math.js, logger.js, userService.js, and app.js. This makes code easier to test, reuse, refactor, and understand as the application grows. The example below demonstrates a small modular application using named exports, a default export, imports, validation, and a simple execution flow.

import { calculateOrderTotal, formatCurrency } from "./orderUtils.js";
import { validateCustomer } from "./validation.js";
import Logger from "./logger.js";

// Create one logger instance for the application.
const logger = new Logger("OrderApp");

// Application data would normally come from an API or database.
const customer = {
  id: 101,
  name: "Ansh",
  email: "ansh@example.com"
};

const order = {
  id: "ORD-2026-001",
  items: [
    { name: "JavaScript Book", price: 799, quantity: 1 },
    { name: "Developer Notebook", price: 249, quantity: 2 },
    { name: "USB-C Hub", price: 1299, quantity: 1 }
  ],
  discount: 100,
  taxRate: 0.18
};

// Step 1: Start the application.
logger.info("Starting modular order processing...");

// Step 2: Validate the customer before processing the order.
logger.info("Validating customer data...");
const customerResult = validateCustomer(customer);

if (!customerResult.valid) {
  logger.error(`Customer validation failed: ${customerResult.message}`);
  process.exit(1);
}

logger.success("Customer validation passed.");

// Step 3: Calculate the order using a separate module.
logger.info(`Calculating order ${order.id}...`);
const result = calculateOrderTotal(order);

// Step 4: Display each important calculation result.
logger.info(`Subtotal: ${formatCurrency(result.subtotal)}`);
logger.info(`Discount: ${formatCurrency(result.discount)}`);
logger.info(`Tax: ${formatCurrency(result.tax)}`);
logger.info(`Final total: ${formatCurrency(result.total)}`);

// Step 5: Print a readable summary for the user.
console.log("\\n========== ORDER SUMMARY ==========");
console.log(`Customer: ${customer.name}`);
console.log(`Order ID: ${order.id}`);
console.log(`Items: ${order.items.length}`);
console.log(`Subtotal: ${formatCurrency(result.subtotal)}`);
console.log(`Discount: -${formatCurrency(result.discount)}`);
console.log(`Tax: ${formatCurrency(result.tax)}`);
console.log(`Total: ${formatCurrency(result.total)}`);
console.log("===================================\\n");

// Step 6: Demonstrate that modules can expose focused functionality.
logger.success("Order processing completed successfully.");
logger.info("Each responsibility is isolated inside its own module.");

// orderUtils.js
export function calculateOrderTotal(order) {
  const subtotal = order.items.reduce((sum, item) => {
    return sum + item.price * item.quantity;
  }, 0);

  const discount = Math.min(order.discount, subtotal);
  const taxableAmount = subtotal - discount;
  const tax = taxableAmount * order.taxRate;
  const total = taxableAmount + tax;

  return {
    subtotal,
    discount,
    tax,
    total
  };
}

export function formatCurrency(value) {
  return new Intl.NumberFormat("en-IN", {
    style: "currency",
    currency: "INR"
  }).format(value);
}

// validation.js
export function validateCustomer(customer) {
  if (!customer) {
    return { valid: false, message: "Customer is required." };
  }

  if (!customer.name || customer.name.trim().length < 2) {
    return { valid: false, message: "Customer name is invalid." };
  }

  if (!customer.email || !customer.email.includes("@")) {
    return { valid: false, message: "Customer email is invalid." };
  }

  return { valid: true, message: "Customer is valid." };
}

// logger.js
export default class Logger {
  constructor(scope) {
    this.scope = scope;
  }

  info(message) {
    console.log(`[INFO] [${this.scope}] ${message}`);
  }

  success(message) {
    console.log(`[SUCCESS] [${this.scope}] ${message}`);
  }

  error(message) {
    console.error(`[ERROR] [${this.scope}] ${message}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

JavaScript modules are more than a convenient way to split files. They create explicit boundaries between parts of an application, reduce global state, clarify dependencies, and make individual pieces of code easier to test and reuse.

A good module should generally have a focused responsibility and expose only the functionality that other parts of the application actually need. Prefer named exports when several related capabilities belong together, and use default exports when there is one clear primary value.

As applications grow, thoughtful module boundaries become increasingly important. Whether you are building a Node.js API, browser application, CLI tool, or larger JavaScript system, mastering modules gives you a foundation for writing cleaner and more maintainable code.

Top comments (0)