12 JS Module System
Let me walk you through JavaScript’s module and dependency system by building on your Python knowledge. Since you’re already comfortable with Python imports, I’ll use those patterns as our starting point.
12.1 The Evolution of JavaScript Modules
JavaScript’s module system has evolved significantly, much like how Python moved from simple scripts to proper package management. Let me show you this progression:
JavaScript Module Evolution:
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ No Modules │───▶│ CommonJS (CJS) │───▶│ ES Modules │
│ (Browser) │ │ (Node.js) │ │ (Modern JS) │
│ │ │ │ │ │
│ - Global scope │ │ - require() │ │ - import/export │
│ - Script tags │ │ - module.exports │ │ - Static analysis│
└─────────────────┘ └──────────────────┘ └─────────────────┘
12.2 Python vs JavaScript: Module Comparison
Let’s start with what you know. In Python, you might have:
# Python: math_utils.py
def add(a, b):
return a + b
def multiply(a, b):
return a * b
= 3.14159
PI
# Python: main.py
from math_utils import add, PI
import math_utils
= add(5, 3)
result1 = math_utils.multiply(4, 2) result2
In modern JavaScript (ES Modules), this looks very similar:
// JavaScript: mathUtils.js
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
export const PI = 3.14159;
// JavaScript: main.js
import { add, PI } from './mathUtils.js';
import * as mathUtils from './mathUtils.js';
const result1 = add(5, 3);
const result2 = mathUtils.multiply(4, 2);
Notice the similarities? The key difference is that JavaScript requires explicit file extensions and relative paths (the ./
prefix).
12.3 CommonJS vs ES Modules
Since you’ll encounter both systems in Node.js development, let me explain the two main approaches:
CommonJS (Traditional Node.js):
// mathUtils.js (CommonJS)
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
// Export individual functions
.add = add;
exports.PI = 3.14159;
exports
// Or export everything at once
.exports = {
module,
add,
multiplyPI: 3.14159
;
}
// main.js (CommonJS)
const { add, PI } = require('./mathUtils');
const mathUtils = require('./mathUtils');
ES Modules (Modern JavaScript):
// mathUtils.js (ES Modules)
export function add(a, b) {
return a + b;
}
export const PI = 3.14159;
// Default export (like Python's main class/function)
export default function calculator() {
return { add, multiply };
}
// main.js (ES Modules)
import calculator, { add, PI } from './mathUtils.js';
import * as mathUtils from './mathUtils.js';
Think of CommonJS as similar to Python’s older import system, while ES Modules are like Python’s modern import syntax with better static analysis capabilities.
12.4 Package Management: npm vs pip
Your experience with pip
translates well to npm
. Here’s the comparison:
Python Package Management JavaScript Package Management
┌─────────────────────────┐ ┌─────────────────────────────┐
│ pip install requests │ ←→ │ npm install axios │
│ requirements.txt │ ←→ │ package.json │
│ pip freeze │ ←→ │ npm list │
│ virtual environments │ ←→ │ node_modules folder │
│ __pycache__ │ ←→ │ node_modules (local cache) │
└─────────────────────────┘ └─────────────────────────────┘
Python requirements.txt:
requests==2.31.0
numpy>=1.21.0 pandas~=2.0.0
JavaScript package.json:
{
"name": "my-project",
"version": "1.0.0",
"dependencies": {
"axios": "^1.4.0",
"lodash": ">=4.17.21",
"moment": "~2.29.0"
},
"devDependencies": {
"jest": "^29.0.0",
"eslint": "^8.0.0"
}
}
12.5 Practical Example: Building a Simple Project
Let me walk you through creating a small project that demonstrates these concepts:
Project Structure:
my-js-project/
├── package.json
├── src/
│ ├── utils/
│ │ ├── mathUtils.js
│ │ └── stringUtils.js
│ ├── services/
│ │ └── apiService.js
│ └── main.js
└── node_modules/ (created by npm)
package.json:
{
"name": "my-js-project",
"version": "1.0.0",
"type": "module", // This enables ES modules
"scripts": {
"start": "node src/main.js",
"dev": "node --watch src/main.js"
},
"dependencies": {
"axios": "^1.4.0"
}
}
src/utils/mathUtils.js:
// Named exports (like Python's from module import function)
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
// Default export (like Python's main class)
export default class Calculator {
constructor() {
this.history = [];
}
calculate(operation, a, b) {
let result;
if (operation === 'add') {
= add(a, b);
result else if (operation === 'multiply') {
} = multiply(a, b);
result
}
this.history.push({ operation, a, b, result });
return result;
} }
src/services/apiService.js:
// Importing external package (like import requests in Python)
import axios from 'axios';
export async function fetchUserData(userId) {
try {
const response = await axios.get(`https://jsonplaceholder.typicode.com/users/${userId}`);
return response.data;
catch (error) {
} console.error('Error fetching user data:', error);
throw error;
}
}
// Multiple named exports
export const API_BASE_URL = 'https://jsonplaceholder.typicode.com';
export function createApiUrl(endpoint) {
return `${API_BASE_URL}/${endpoint}`;
}
src/main.js:
// Importing from local modules
import Calculator, { add, multiply } from './utils/mathUtils.js';
import { fetchUserData, API_BASE_URL } from './services/apiService.js';
// Using the imports
const calc = new Calculator();
console.log('Direct function:', add(5, 3));
console.log('Calculator class:', calc.calculate('multiply', 4, 7));
// Async operation (similar to Python's asyncio)
async function main() {
try {
const userData = await fetchUserData(1);
console.log('User data:', userData.name);
catch (error) {
} console.error('Failed to fetch user data');
}
}
main();
12.6 Key Differences from Python
While the concepts are similar, here are important differences to remember:
File Extensions: JavaScript requires explicit file extensions in import paths, while Python doesn’t:
// JavaScript
import { add } from './mathUtils.js'; // .js required
// Python
from math_utils import add # no .py needed
Relative vs Absolute Imports: JavaScript uses explicit relative paths:
// JavaScript
import { add } from './utils/mathUtils.js'; // relative
import axios from 'axios'; // package
// Python
from .utils.math_utils import add # relative
import requests # package
Default Exports: JavaScript has a special “default export” concept that Python doesn’t have:
// JavaScript - you can have ONE default export per module
export default function Calculator() { ... }
import Calculator from './calculator.js'; // no braces needed
// Python equivalent would be importing the main class
from calculator import Calculator
12.7 Working with Node.js Packages
Installing and using packages works similarly to Python, but with some JavaScript-specific patterns:
# Install a package (like pip install)
npm install lodash
# Install development dependencies (like pip install --dev)
npm install --save-dev jest
# Install globally (like pip install --global)
npm install -g nodemon
Using installed packages:
// Similar to: import pandas as pd
import _ from 'lodash';
// Similar to: from requests import get, post
import { get, post } from 'axios';
// Using the package
const numbers = [1, 2, 3, 4, 5];
const doubled = _.map(numbers, n => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
12.8 Mental Model: Think of Modules as Files
Here’s a helpful mental model coming from Python: in JavaScript, every file is potentially a module. Unlike Python where you can have multiple classes in one file and import them separately, JavaScript encourages one main thing per file:
Python Approach: JavaScript Approach:
┌─────────────────┐ ┌─────────────────┐
│ utilities.py │ │ mathUtils.js │
│ │ │ │
│ class Math: │ vs │ export class │
│ def add() │ │ Math { ... } │
│ │ └─────────────────┘
│ class String: │ ┌─────────────────┐
│ def clean() │ │ stringUtils.js │
│ │ │ │
│ class Date: │ │ export class │
│ def format() │ │ StringUtils │
└─────────────────┘ └─────────────────┘
This makes JavaScript modules more predictable and easier to tree-shake (remove unused code) during bundling.