JavaScript Modules (ESM & CommonJS)

Pre-Work

Be sure to confirm that you have the appropriate GitHub Classroom repo link. When directed to do so in this lesson, you will need to follow along with the code examples and replicate those. Commit early, commit often!

Introduction

  • Icebreaker: Have you ever tried to organize a big project or group assignment? How did you keep everyone's work from getting mixed up?

  • Real-world scenario: Imagine building a house—each room is built separately, then connected together. JavaScript modules let you build your code in “rooms” (files) and connect them as needed.

  • Lesson objectives:

    • Understand what modules are and why they matter
    • Learn the basics of ES Modules (ESM)
    • Recognize CommonJS for legacy code
    • Know when and how to use "type": "module". HINT: Always for new projects!

Core Concept Overview

The Problem Modules Solve

Mental model: Imagine you're writing a book. You could write everything in one giant chapter, but that would be:

  • Hard to navigate and find specific topics
  • Difficult for multiple authors to work on simultaneously
  • Impossible to reuse sections in other books

As your JavaScript projects grow, you face the same challenges:

// Everything in one file becomes unwieldy:
// 500 lines of mixed functions for users, products, payments, etc.
// Hard to find anything, hard to test, hard to collaborate

Real-world analogy: Think about how a restaurant kitchen works. Instead of one person doing everything, you have:

  • Prep cook: Handles vegetables and basic preparations
  • Grill cook: Manages all grilled items
  • Pastry chef: Focuses on desserts
  • Expediter: Coordinates and combines everything

Each person has specialized responsibilities and clear interfaces for working together. JavaScript modules work the same way!

What is a Module?

A module is a file that contains code with a specific purpose and defined ways to share that code with other files.

Key benefits:

  • Organization: Related functions live together (all math functions in math.js)
  • Reusability: Write once, use everywhere (your calculateGrade function in multiple projects)
  • Collaboration: Team members can work on different modules simultaneously
  • Maintenance: Fix a bug in one place instead of hunting through massive files
  • Testing: Test each module independently

Mental model: Think of modules like LEGO blocks—each piece has a specific shape and function, with clear connection points. You can combine them to build complex structures, and you can reuse the same pieces in different creations.

The Module Interface Concept

Before modules, sharing code meant copying and pasting or putting everything in one file:

// The old way: everything mixed together
const userName = "John";
const userAge = 25;
const calculateTax = (amount) => amount * 0.08;
const formatCurrency = (amount) => `$${amount.toFixed(2)}`;
const productName = "Widget";
const productPrice = 29.99;
// ... 500 more lines mixing user, tax, product logic

With modules, each file has a clear interface—what it offers to other files:

// tax.js - focused on tax calculations
export const calculateTax = (amount) => amount * 0.08;
export const getTaxRate = () => 0.08;

// currency.js - focused on money formatting
export const formatCurrency = (amount) => `$${amount.toFixed(2)}`;

// main.js - uses the interfaces
import { calculateTax } from "./tax.js";
import { formatCurrency } from "./currency.js";

Mental model: Think of each module like a vending machine:

  • Input slots: What data/parameters it accepts
  • Output slot: What it gives back (exported functions/values)
  • Internal mechanism: How it works (implementation details you don't need to know)

JavaScript Module History: Why ESM Won

JavaScript modules have had a messy history, but there's now a clear winner:

  1. ES Modules (ESM) - The modern standard ✅
  2. CommonJS - Legacy Node.js system that refuses to die 💀

The reality:

  • ESM: What you should use for ALL new projects
  • CommonJS: Old syntax you'll unfortunately still encounter in outdated codebases

Mental model: It's like still seeing houses with knob-and-tube wiring occasionally, even though modern electrical systems are clearly superior. CommonJS is the knob-and-tube wiring of JavaScript modules.

ES Modules (ESM) - The Modern Standard

  • Standard for modern JavaScript (in browsers and Node.js)
  • Use import and export keywords to share code between files

Named Exports

You can export multiple things from a file by name:

// math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;

Import them by name (curly braces):

import { add, subtract } from "./math.js";
console.log(add(2, 3)); // 5
Named Imports & Object Destructuring: What's the Connection?

The syntax for importing named exports is just like object destructuring:

// Object destructuring
const { add, subtract } = mathUtils;

// Module import (very similar!)
import { add, subtract } from "./math.js";

In both cases, you’re “pulling out” specific properties by name.

import { add, subtract } from "./math.js";
console.log(add(2, 3)); // 5

Default Exports

Each file can have one default export:

// greet.js
const greet = (name) => `Hello, ${name}!`;
export default greet;

Import default exports without curly braces (you can name it whatever you want):

import greet from "./greet.js";
console.log(greet("Sam")); // Hello, Sam!

Renaming Exports on Import

You can rename things as you import them:

import { add as sum } from "./math.js";
console.log(sum(2, 3)); // 5

Export Best Practices

Prefer default exports when a module has one main thing:

// ✅ Good: Function component = default export
// user-profile.js
// Note that function components are typically capitalized
const UserProfile = (name, email, role) => {
  return `
    <div class="user-profile">
      <h2>${name}</h2>
      <p>Email: ${email}</p>
      <p>Role: ${role}</p>
    </div>
  `;
};

export default UserProfile;

// In another file (e.g., main.js) - import cleanly
import UserProfile from "./user-profile.js";
const profileHTML = UserProfile("Maria", "maria@example.com", "student");

Use named exports for utility collections:

// ✅ Good: Multiple utilities = named exports
// user-utils.js
export const login = (email, password) => {
  console.log(`Logging in ${email}`);
  return { success: true, user: email };
};

export const logout = () => {
  console.log("Logging out");
  return { success: true };
};

export const getCurrentUser = () => {
  return "current-user@example.com";
};

// Import what you need
import { login, getCurrentUser } from "./user-utils.js";

❌ Don't mix default and named exports in the same file:

// ❌ Confusing: mixing both patterns
export const helper1 = () => {
  /* ... */
};
export const helper2 = () => {
  /* ... */
};
export default mainFunction; // Now consumers don't know what's "main"

// ❌ Leads to inconsistent imports
import mainFunction, { helper1 } from "./mixed.js"; // Confusing!

Mental model: Each module should have a clear identity:

  • "I'm a service" → Default export
  • "I'm a toolbox" → Named exports
  • "I'm both" → Pick one pattern and stick to it!

File naming convention: Use kebab-case for JavaScript file names (e.g., user-service.js, math-utils.js) to maintain consistency with modern web development practices.

Node.js Notes

  • You must set "type": "module" in your package.json to use ESM syntax.
  • File extensions (.js, .mjs) matter!
    • If your project uses "type": "module", you can use .js for ESM files.
    • Sometimes, especially in older Node.js or when mixing module types, you'll see .mjs for ESM files. Just know .mjs means "this is a module"—but for most new projects, .js is enough.

CommonJS (The Legacy System You'll Unfortunately Encounter)

Why mention it? Because you'll still see this outdated syntax in older projects that haven't been properly updated:

// Old CommonJS syntax - DON'T use this for new projects
// math.js
module.exports = {
  add: (a, b) => a + b,
};

// main.js
const { add } = require("./math.js");
console.log(add(2, 3)); // 5

Key points:

  • Don't write new code this way
  • 👀 Just recognize it when you see it in old tutorials or legacy codebases
  • 🔄 Migrate away from it when possible

Why ESM is better: Cleaner syntax, browser support, better tooling, static analysis, and it's the actual JavaScript standard.

When to Use "type": "module"

  • Add "type": "module" to your package.json to enable ESM in Node.js projects.
  • Without it, Node.js treats files as CommonJS by default.
{
  "type": "module"
}

Hands-On Application

Exercise 1: Recognizing the Need for Modules

Scenario: Imagine you're building a student grade calculator that's grown into this single file:

// Everything mixed together in one file - getting messy!
const student1 = { name: "Alice", grades: [85, 92, 78] };
const student2 = { name: "Bob", grades: [95, 87, 91] };

const calculateAverage = (grades) => {
  const sum = grades.reduce((total, grade) => total + grade, 0);
  return sum / grades.length;
};

const getLetterGrade = (average) => {
  if (average >= 90) return "A";
  if (average >= 80) return "B";
  if (average >= 70) return "C";
  if (average >= 60) return "D";
  return "F";
};

const formatGradeReport = (student) => {
  const avg = calculateAverage(student.grades);
  const letter = getLetterGrade(avg);
  return `${student.name}: ${avg.toFixed(1)}% (${letter})`;
};

// More functions for attendance, assignments, etc...
// This file is getting huge and hard to navigate!

Question for reflection: What problems do you see with this approach as the project grows?

Exercise 2: Planning Module Organization

Before writing code, think about how you'd organize the above functions into logical modules. Consider:

  • Which functions naturally belong together?
  • What would you name each module file?
  • Which functions would other modules need to import?

Your turn: Write down your module organization plan:

- _________.js: (what functions?)
- _________.js: (what functions?)
- _________.js: (what functions?)

Exercise 3: Your First Module

Let's start simple. Create two files:

greetings.js - A module that handles different types of greetings:

export const sayHello = (name) => `Hello, ${name}!`;
export const sayGoodbye = (name) => `Goodbye, ${name}!`;
export const welcomeMessage = "Welcome to our application!";

main.js - Uses the greetings module:

import { sayHello, welcomeMessage } from "./greetings.js";

console.log(welcomeMessage);
console.log(sayHello("Student"));

Try it: What happens if you try to access sayGoodbye in main.js without importing it?

Exercise 4: Default Export Practice

Create a calculator.js with a default export:

const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
const multiply = (a, b) => a * b;

/**
 * Ternary to handle divide by zero
 *
 * IF b is NOT equal to 0, THEN return a divided by b
 * ELSE return "Cannot divide by zero"
 */
const divide = (a, b) => (b !== 0 ? a / b : "Cannot divide by zero"); // ternary to handle divide by zero

// Group functions into a single calculator object for export
const calculator = { add, subtract, multiply, divide };

export default calculator;

Then import and use it:

import calc from "./calculator.js";

console.log(calc.add(5, 3)); // 8

Observe: Notice how you can name the default import anything you want (calc, calculator, math, etc.).

Assessment: Personal Reflection

Take a few minutes to reflect on what you’ve learned about JavaScript modules. In your own words, answer the following:

  • Why do you think modules are important for larger projects?
  • What’s the difference between a named export and a default export?
  • When might you want to rename an import?

Write a short paragraph (3–5 sentences) with your thoughts. There are no wrong answers—focus on your understanding and any questions you still have!