JAVASCRIPT โ€ข GETTING STARTED AND TOOLING

Module Systems โ€” Use module systems at a high level (CommonJS vs ES Modules) (conceptual)

Learn how JavaScript organizes code into reusable, self-contained pieces using two major module systems.

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
Brendan Eich creates JavaScript at Netscape. All code runs in a single global scope with no module support.
2009
Node.js & CommonJS
Ryan Dahl launches Node.js for server-side JavaScript. It adopts the CommonJS module specification, using require() and module.exports.
2015
ES Modules Standardized
ECMAScript 2015 (ES6) introduces ES Modules with import and export keywords as part of the official language standard.
2018โ€“2020
Browser & Node.js Support
Major browsers ship native ES Module support. Node.js adds experimental, then stable, ES Module support alongside CommonJS.

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.

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.

1

Encapsulation

Each module has its own private scope. Variables and functions defined inside a module are hidden from the outside world unless you explicitly export them. This prevents naming collisions.
2

Reusability

A well-written module can be imported into many different projects without rewriting the code. Libraries like Lodash or React are distributed as modules.
3

Dependency Management

Modules explicitly declare what other modules they depend on. This makes the relationship between files clear and lets tools resolve the correct loading order automatically.
4

Maintainability

Smaller, focused files are easier to read, test, and debug. When a bug appears, you can isolate the problem to a single module instead of searching through thousands of lines.
โœฆ KEY TAKEAWAY
Think of modules like rooms in a house. Each room has its own purpose and its own stuff inside. You don't dump everything into one giant room โ€” you separate the kitchen from the bedroom. A module's exports are like the doors and windows โ€” they control what the rest of the house can see and use.

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.

Module A contains four declarations but only exports two of them (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.

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
Exporting: 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
Named export: 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.

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.

CommonJS vs ES Modules at a glance
FeatureCommonJS (CJS)ES Modules (ESM)
Import syntaxrequire('./file')import x from './file'
Export syntaxmodule.exports = ...export / export default
LoadingSynchronous (blocking)Asynchronous (non-blocking)
AnalysisDynamic โ€” resolved at runtimeStatic โ€” resolved before execution
Tree shakingNot natively supportedSupported โ€” unused exports removed
File extension.js (default in Node).mjs or .js with "type": "module"
Primary environmentNode.js (server)Browsers & modern Node.js
CommonJS (left) reads and executes the entire file synchronously before moving on. ES Modules (right) first parse the dependency graph, fetch files asynchronously, create live bindings, and then execute โ€” a multi-phase process that enables optimizations and works well over the network.

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.

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 to module.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)Use require() 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 functionsCall the functions just like any other function. The private internals of math.js remain inaccessible.
console.log(add(3, 5)); // 8 console.log(subtract(10, 4)); // 6
Creating and Using a Module โ€” ES Modules
1
Step 1 โ€” Create the module file (math.mjs)Use the export keyword 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 the import keyword 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 functionsThe usage is identical. The difference is only in how the files are linked together.
console.log(add(3, 5)); // 8 console.log(subtract(10, 4)); // 6
โœฆ KEY TAKEAWAY
Both module systems accomplish the same goal โ€” splitting code into separate files and sharing specific pieces. CommonJS uses 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).

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.

CriteriaCommonJS 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
If you are starting a brand-new project today, ES Modules are the recommended choice because they are the official standard and are supported everywhere modern JavaScript runs. However, you will encounter CommonJS constantly in existing projects, tutorials, and npm packages, so you need to be comfortable reading both syntaxes.

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.

ConceptWhat You Know NowWhere It Leads
ModulesSplitting code into files with import/exportBundlers (Webpack, Vite) that combine modules for production
Static importsimport/export at the top of the fileDynamic import() for lazy loading and code splitting
npm packagesThird-party modules installed via npmPackage management, lockfiles, and monorepos
EncapsulationPrivate scope within each moduleDesign 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.

Practice Problems

PROBLEM 1 โ€” CONCEPTUAL
In your own words, explain why JavaScript needed module systems. What specific problems arise when all code is written in a single file or in a global scope?
PROBLEM 2 โ€” BASIC CALCULATION
You have a file called 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.
PROBLEM 3 โ€” INTERMEDIATE
Convert the following CommonJS code to ES Module syntax: const axios = require('axios'); and module.exports = { fetchData };. Explain one advantage that the ES Module version gains over the CommonJS version.
PROBLEM 4 โ€” APPLIED
You are building a web application that runs in the browser. A teammate suggests using require() to load modules. Explain why this will not work directly in the browser and recommend an alternative approach.
PROBLEM 5 โ€” CRITICAL THINKING
ES Modules require all 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.

Lesson 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.

Varsity Tutors โ€ข JavaScript โ€ข Module Systems โ€” Use module systems at a high level (CommonJS vs ES Modules) (conceptual)