SECTION 1
Historical Context & Motivation
When JavaScript was first created in 1995, it was designed to add small bits of interactivity to web pages — things like form validation or simple animations. A typical script might be only a dozen lines long, and everything lived inside a single file. There was no built-in way to split code across multiple files and share functionality between them, because nobody imagined that JavaScript programs would ever grow beyond a few hundred lines.
Fast-forward to today, and JavaScript applications regularly contain thousands — even millions — of lines of code. Without a strategy for organizing all that code, developers would face a nightmare of naming collisions (two variables accidentally sharing the same name), tangled dependencies (file A relies on file B which relies on file C), and code that is nearly impossible to debug. The solution to this chaos is the module system — a standardized way to break a program into smaller, self-contained files that can export functionality and import what they need from other files.
1995 • JavaScript Is Born
2009 • Node.js & CommonJS
require() and module.exports.2015 • ES Modules Standardized
import and export keywords as part of the official language standard.2018–2020 • Browser & Node.js Support
This history leaves us with two major module systems that you will encounter in the JavaScript ecosystem. Understanding how they differ — and when to use each — is a foundational skill for any JavaScript developer today.
SECTION 2
Core Principles of Modules
Before diving into the specific syntax of CommonJS or ES Modules, it helps to understand the core ideas that any module system is trying to achieve. These principles explain why modules exist and what problems they solve.
Encapsulation
Reusability
Dependency Management
Maintainability
KEY TAKEAWAY
SECTION 3
How Modules Connect — Visual Overview
The diagram below shows the fundamental pattern shared by both CommonJS and ES Modules. A source module defines private code and selectively exposes pieces through exports. A consuming module then imports only the pieces it needs, without gaining access to the private internals.
greet and PI). Module B imports those exports but cannot access the private variables secret or counter.This export-import pattern is the beating heart of both CommonJS and ES Modules. The difference between the two systems lies in how you write the export and import statements, when the code is loaded, and where each system is most commonly used. We will explore those differences next.
SECTION 4
How Each Module System Works
CommonJS (CJS)
CommonJS was created for Node.js and is the default module system on the server side. It uses two key pieces of syntax: require() to import a module and module.exports (or the shorthand exports) to export values. CommonJS loads modules synchronously, meaning the program waits for the file to be fully read and executed before moving on. This works fine on a server where files are on the local disk, but it can cause delays in a browser that has to download files over the internet.
CommonJS Syntax at a Glance
module.exports = { greet, PI }; • Importing: const { greet, PI } = require('./moduleA'); The require() call can appear anywhere in the file, even inside an if block — it is a regular function call.ES Modules (ESM)
ES Modules are the official standard built into the JavaScript language since ES6 (2015). They use the keywords import and export. Unlike CommonJS, ES Modules are loaded asynchronously, which makes them ideal for the browser. The import and export statements must appear at the top level of the file — you cannot place them inside a function or an if block. This restriction lets JavaScript engines analyze the module structure before running any code, enabling powerful optimizations like tree shaking (removing unused exports from the final bundle).
ES Module Syntax at a Glance
export function greet() { ... } • Default export: export default function greet() { ... } • Named import: import { greet } from './moduleA.js'; • Default import: import greet from './moduleA.js';A key conceptual difference is static vs. dynamic analysis. Because CommonJS uses a regular function (require()), JavaScript cannot know what a module exports until the code actually runs. ES Module import/export statements are statically analyzable — the engine can map out the entire dependency graph before any code executes.
SECTION 5
Side-by-Side Comparison
The table below summarizes the key differences between CommonJS and ES Modules. Refer back to this chart whenever you need a quick refresher.
| Feature | CommonJS (CJS) | ES Modules (ESM) |
|---|---|---|
| Import syntax | require('./file') | import x from './file' |
| Export syntax | module.exports = ... | export / export default |
| Loading | Synchronous (blocking) | Asynchronous (non-blocking) |
| Analysis | Dynamic — resolved at runtime | Static — resolved before execution |
| Tree shaking | Not natively supported | Supported — unused exports removed |
| File extension | .js (default in Node) | .mjs or .js with "type": "module" |
| Primary environment | Node.js (server) | Browsers & modern Node.js |
CommonJS vs ES Modules at a glance
Notice the label "live bindings" on the ES Modules side. When you import a value with ESM, you get a live reference to the original variable — if the source module updates it, your import automatically reflects the change. In CommonJS, require() returns a copy of the exported value at the moment of import. This subtle difference rarely matters for beginners, but it becomes significant in advanced patterns.
SECTION 6
Worked Example — Building a Utility Module
Let's walk through creating a small utility module in both CommonJS and ES Module syntax. Imagine you are building a calculator app and you want to keep your math functions in a separate file so that any part of your project can reuse them.
Creating and Using a Module — CommonJS
- 1
Step 1 — Create the module file (math.js)
Define two functions and assign them tomodule.exports. Anything not exported stays private.function add(a, b) { return a + b; }function subtract(a, b) { return a - b; }module.exports = { add, subtract }; - 2
Step 2 — Import in another file (app.js)
Userequire()with the relative path to the module file. Destructure the object to grab only the functions you need.const { add, subtract } = require('./math'); - 3
Step 3 — Use the imported functions
Call the functions just like any other function. The private internals ofmath.jsremain inaccessible.console.log(add(3, 5)); // 8console.log(subtract(10, 4)); // 6
Creating and Using a Module — ES Modules
- 1
Step 1 — Create the module file (math.mjs)
Use theexportkeyword directly in front of each declaration you want to make available.export function add(a, b) { return a + b; }export function subtract(a, b) { return a - b; } - 2
Step 2 — Import in another file (app.mjs)
Use theimportkeyword at the top of the file. Note the curly braces for named imports and the file extension in the path.import { add, subtract } from './math.mjs'; - 3
Step 3 — Use the imported functions
The usage is identical. The difference is only in how the files are linked together.console.log(add(3, 5)); // 8console.log(subtract(10, 4)); // 6
KEY TAKEAWAY
require() / module.exports and runs synchronously. ES Modules use import / export and run asynchronously. Think of it like texting (async — you send and keep doing other things) versus a phone call (sync — you wait until the other person finishes talking).SECTION 7
Strengths & Limitations
Neither module system is universally "better" — each has strengths and trade-offs depending on the environment and use case. The table below helps you decide which one fits a given situation.
| Criteria | CommonJS Strength? | ES Modules Strength? |
|---|---|---|
| Browser support | ❌ Not natively supported in browsers | ✅ Natively supported in all modern browsers |
| Node.js ecosystem | ✅ Vast existing package library uses CJS | ⚠️ Growing adoption; some older packages CJS-only |
| Tree shaking | ❌ Not possible (dynamic analysis) | ✅ Bundlers can remove unused exports |
| Conditional imports | ✅ require() works inside if blocks | ⚠️ Needs dynamic import() for conditional loading |
| Future direction | ⚠️ Legacy; maintained but not expanding | ✅ The official JavaScript standard going forward |
PRACTICAL ADVICE
SECTION 8
Connection to Advanced Tooling
Understanding module systems at a conceptual level opens the door to more advanced tools in the JavaScript ecosystem. Tools called bundlers — like Webpack, Vite, and Rollup — take your many module files and combine them into one or a few optimized files for deployment. Bundlers rely heavily on the static analysis capabilities of ES Modules to perform tree shaking, code splitting, and lazy loading.
| Concept | What You Know Now | Where It Leads |
|---|---|---|
| Modules | Splitting code into files with import/export | Bundlers (Webpack, Vite) that combine modules for production |
| Static imports | import/export at the top of the file | Dynamic import() for lazy loading and code splitting |
| npm packages | Third-party modules installed via npm | Package management, lockfiles, and monorepos |
| Encapsulation | Private scope within each module | Design patterns like the Revealing Module Pattern, classes |
As you continue learning, you will also encounter TypeScript, which adds type checking on top of ES Module syntax, and Deno, a newer JavaScript runtime that uses ES Modules exclusively — it dropped CommonJS entirely. The trend across the ecosystem is clearly moving toward ES Modules as the universal standard.
SECTION 9
Practice Problems
Question 1: CONCEPTUAL
Show answer
Question 2: BASIC CALCULATION
helpers.js that contains a function capitalize. Write the one line of code needed to export it using CommonJS, and then write the one line needed to import it in another file called app.js.Show answer
helpers.js, you would write: module.exports = { capitalize }; — this makes the function available to other files. In app.js, you would write: const { capitalize } = require('./helpers'); — this imports the function using destructuring. The './' indicates the file is in the same directory.Question 3: INTERMEDIATE
const axios = require('axios'); and module.exports = { fetchData };. Explain one advantage that the ES Module version gains over the CommonJS version.Show answer
import axios from 'axios'; (this is a default import since axios typically uses a default export) and export { fetchData }; (a named export). One key advantage is that the ES Module version enables tree shaking — if fetchData is exported but never imported by any consumer, a bundler can safely remove it from the final production code, reducing the file size sent to users.Question 4: APPLIED
require() to load modules. Explain why this will not work directly in the browser and recommend an alternative approach.Show answer
require() function is provided by Node.js and relies on synchronous file-system access, which browsers cannot perform. Instead, you should use ES Module syntax (import/export) with <script type="module"> tags in your HTML. Alternatively, you could use a bundler like Webpack or Vite to transform CommonJS code into a browser-compatible bundle, but the recommended approach for new projects is to write in ES Module syntax from the start.Question 5: CRITICAL THINKING
import statements to be at the top level (not inside functions or conditionals), while CommonJS allows require() anywhere. Argue why the ES Module restriction is actually a feature, not a limitation. Then describe one scenario where the flexibility of CommonJS require() is genuinely useful.Show answer
require() is useful when you genuinely need to load a module conditionally at runtime — for example, loading a different database driver depending on an environment variable (const db = require(process.env.DB_TYPE === 'postgres' ? 'pg' : 'mysql');). ES Modules handle this scenario with the dynamic import() function, which returns a Promise, but the syntax is more complex.SUMMARY
Lesson Summary
Summary
JavaScript module systems solve the problem of organizing large codebases by splitting code into self-contained files with private scopes. The two major systems are CommonJS — which uses require() and module.exports, loads synchronously, and is the traditional Node.js standard — and ES Modules — which uses import and export keywords, loads asynchronously, and is the official JavaScript language standard.
Both systems achieve encapsulation, reusability, and dependency management. ES Modules offer the additional advantage of static analysis, enabling tree shaking and other optimizations. For new projects, ES Modules are the recommended choice, but fluency in CommonJS is essential because it remains widespread across the npm ecosystem and existing Node.js codebases.