As applications grow, organizing code becomes critical. JavaScript modules (ES6 modules) provide a standardized way to split code into separate files, where each file can export functionality and import what it needs from other modules. This creates clean, maintainable, testable code with clear dependencies. Modules eliminate global namespace pollution and enable code reuse across projects. Let's master JavaScript's module system!
Why Use Modules?
Benefits of Modules:
- Code organization - logical file structure
- Reusability - import functionality anywhere
- Encapsulation - private by default, explicit exports
- Maintainability - easier to find and fix bugs
- No global namespace pollution
- Clear dependencies - see what each file needs
- Better for bundlers and tree-shaking
Modules organize code better
Without Modules
// Old way: Everything in one file
// main.js (1000+ lines)
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
function multiply(a, b) {
return a * b;
}
class User {
constructor(name) {
this.name = name;
}
}
class Product {
constructor(name, price) {
this.name = name;
this.price = price;
}
}
// ... hundreds more lines
// Hard to navigate!
// Everything in global scope!With Modules
// Modern way: Separate modules
// math.js
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
// models/User.js
export class User {
constructor(name) {
this.name = name;
}
}
// main.js
import { add, subtract } from './math.js';
import { User } from './models/User.js';
// Clean, organized!
// Import only what you need!Basic Module Syntax
Exporting from a Module
// File: math.js
// Named exports (multiple per file)
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
export const PI = 3.14159;
export class Calculator {
multiply(a, b) {
return a * b;
}
}
// Alternative: Export at end
function divide(a, b) {
return a / b;
}
function power(a, b) {
return a ** b;
}
export { divide, power };
// You can also rename on export
function square(x) {
return x * x;
}
export { square as sq };Importing from a Module
// File: main.js
// Import named exports (must use exact names)
import { add, subtract } from './math.js';
console.log(add(5, 3)); // 8
console.log(subtract(5, 3)); // 2
// Import multiple items
import { add, subtract, PI, Calculator } from './math.js';
console.log(PI); // 3.14159
let calc = new Calculator();
// Import with rename (alias)
import { add as addition, subtract as subtraction } from './math.js';
console.log(addition(5, 3)); // 8
// Import everything as an object
import * as math from './math.js';
console.log(math.add(5, 3)); // 8
console.log(math.PI); // 3.14159
let calc2 = new math.Calculator();Default Exports
A module can have one default export (the main thing the module provides).
// File: User.js
// Default export (one per file)
export default class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
greet() {
console.log(`Hello, I'm ${this.name}`);
}
}
// Alternative syntax
class User {
// ... class definition
}
export default User;
// Or with functions
export default function greet(name) {
console.log(`Hello, ${name}`);
}
// Or with values
export default { name: 'App', version: '1.0.0' };Importing Default Exports
// File: main.js
// Import default export (can use any name!)
import User from './User.js';
let user = new User('Alice', 'alice@test.com');
user.greet(); // "Hello, I'm Alice"
// Can name it anything
import MyUser from './User.js';
import Person from './User.js';
import U from './User.js';
// All work - importing the same default export
// Import default along with named exports
// File: utils.js
export default function main() {
console.log('Main function');
}
export function helper() {
console.log('Helper function');
}
// Import both
import main, { helper } from './utils.js';
main(); // "Main function"
helper(); // "Helper function"Mixing Named and Default Exports
// File: database.js
// Default export (main functionality)
export default class Database {
constructor(url) {
this.url = url;
}
connect() {
console.log(`Connecting to ${this.url}`);
}
}
// Named exports (utilities)
export function validateConnection(url) {
return url.startsWith('mongodb://');
}
export const DEFAULT_TIMEOUT = 5000;
export class ConnectionError extends Error {
constructor(message) {
super(message);
this.name = 'ConnectionError';
}
}
// --- Using the module ---
// File: app.js
// Import default and named exports together
import Database, {
validateConnection,
DEFAULT_TIMEOUT,
ConnectionError
} from './database.js';
let url = 'mongodb://localhost:27017';
if (validateConnection(url)) {
let db = new Database(url);
db.connect();
}Common Import Patterns
1. Importing Specific Items
// Import only what you need
import { add, multiply } from './math.js';
// Good for tree-shaking (removing unused code)
// Bundle only includes 'add' and 'multiply'2. Import All as Namespace
// Import everything as an object
import * as math from './math.js';
// Access as properties
math.add(5, 3);
math.multiply(2, 4);
math.PI;
// Good when you need many exports
// Clear namespace (math.add vs just add)3. Import with Aliases
// Rename on import to avoid conflicts
import { add as mathAdd } from './math.js';
import { add as stringAdd } from './strings.js';
mathAdd(5, 3); // Number addition
stringAdd('a', 'b'); // String concatenation
// Or rename multiple
import {
longFunctionName as short,
anotherLongName as another
} from './module.js';4. Side-Effect Imports
// Import module for its side effects only
// (module runs but doesn't export anything we need)
import './polyfills.js'; // Adds polyfills to global
import './styles.css'; // Loads CSS (with bundler)
import './analytics.js'; // Initializes tracking
// Module code runs, but nothing imported5. Dynamic Imports
// Import modules dynamically (at runtime)
async function loadModule() {
// import() returns a Promise
const module = await import('./math.js');
console.log(module.add(5, 3)); // 8
}
// Conditional loading
if (condition) {
import('./heavy-module.js')
.then(module => {
module.doSomething();
});
}
// Load on demand (code splitting)
button.addEventListener('click', async () => {
const { openModal } = await import('./modal.js');
openModal();
});Module File Structure
project/
│
├── src/
│ ├── utils/
│ │ ├── math.js (math utilities)
│ │ ├── string.js (string utilities)
│ │ └── validation.js (validation functions)
│ │
│ ├── models/
│ │ ├── User.js (User class)
│ │ ├── Product.js (Product class)
│ │ └── Order.js (Order class)
│ │
│ ├── services/
│ │ ├── api.js (API calls)
│ │ ├── auth.js (authentication)
│ │ └── database.js (database operations)
│ │
│ ├── components/
│ │ ├── Header.js (Header component)
│ │ ├── Footer.js (Footer component)
│ │ └── Button.js (Button component)
│ │
│ └── main.js (entry point)
│
└── index.htmlPractical Examples
Example 1: Utility Functions Module
// File: utils/math.js
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
export function multiply(a, b) {
return a * b;
}
export function divide(a, b) {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
export const PI = 3.14159;
export const E = 2.71828;
// File: utils/string.js
export function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
export function reverse(str) {
return str.split('').reverse().join('');
}
export function truncate(str, length) {
if (str.length <= length) return str;
return str.slice(0, length) + '...';
}
// File: main.js
import { add, multiply, PI } from './utils/math.js';
import { capitalize, truncate } from './utils/string.js';
console.log(add(5, 3)); // 8
console.log(PI); // 3.14159
console.log(capitalize('hello')); // "Hello"
console.log(truncate('Long text here', 8)); // "Long tex..."Example 2: Class-Based Module
// File: models/User.js
export default class User {
constructor(name, email) {
this.name = name;
this.email = email;
this.createdAt = new Date();
}
greet() {
return `Hello, I'm ${this.name}`;
}
static validateEmail(email) {
return email.includes('@');
}
}
export class UserManager {
constructor() {
this.users = [];
}
addUser(user) {
this.users.push(user);
}
findByEmail(email) {
return this.users.find(u => u.email === email);
}
}
// File: main.js
import User, { UserManager } from './models/User.js';
let user1 = new User('Alice', 'alice@test.com');
let user2 = new User('Bob', 'bob@test.com');
let manager = new UserManager();
manager.addUser(user1);
manager.addUser(user2);
console.log(manager.findByEmail('alice@test.com'));Example 3: API Service Module
// File: services/api.js
const BASE_URL = 'https://api.example.com';
export async function fetchUsers() {
const response = await fetch(`${BASE_URL}/users`);
if (!response.ok) throw new Error('Failed to fetch users');
return response.json();
}
export async function fetchUserById(id) {
const response = await fetch(`${BASE_URL}/users/${id}`);
if (!response.ok) throw new Error('User not found');
return response.json();
}
export async function createUser(userData) {
const response = await fetch(`${BASE_URL}/users`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});
if (!response.ok) throw new Error('Failed to create user');
return response.json();
}
export async function updateUser(id, userData) {
const response = await fetch(`${BASE_URL}/users/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});
if (!response.ok) throw new Error('Failed to update user');
return response.json();
}
export async function deleteUser(id) {
const response = await fetch(`${BASE_URL}/users/${id}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('Failed to delete user');
return true;
}
// File: main.js
import {
fetchUsers,
fetchUserById,
createUser
} from './services/api.js';
async function loadUserData() {
try {
let users = await fetchUsers();
console.log('All users:', users);
let user = await fetchUserById(1);
console.log('User 1:', user);
let newUser = await createUser({
name: 'Charlie',
email: 'charlie@test.com'
});
console.log('Created user:', newUser);
} catch (error) {
console.error('Error:', error.message);
}
}
loadUserData();Example 4: Configuration Module
// File: config.js
export default {
api: {
baseURL: 'https://api.example.com',
timeout: 5000,
headers: {
'Content-Type': 'application/json'
}
},
app: {
name: 'My App',
version: '1.0.0',
environment: 'development'
},
features: {
enableAnalytics: true,
enableNotifications: false,
maxUploadSize: 5242880 // 5MB
}
};
// File: services/api.js
import config from './config.js';
async function fetchData(endpoint) {
const response = await fetch(`${config.api.baseURL}${endpoint}`, {
headers: config.api.headers
});
return response.json();
}
// File: main.js
import config from './config.js';
console.log(`Running ${config.app.name} v${config.app.version}`);
if (config.features.enableAnalytics) {
initAnalytics();
}JavaScript Modules
Master import/export for code organization
console.log() to see your output in the console above.Important Notes
Module Requirements:
- Modules must be served via HTTP(S), not file:// protocol
- Use
type="module"in script tag:<script type="module" src="main.js"> - Modules are automatically in strict mode
- Modules are deferred by default (like defer attribute)
- Each module has its own scope (not global)
- File extensions (.js) are required in import paths in browsers
<!-- HTML file -->
<!DOCTYPE html>
<html>
<head>
<title>Modules Example</title>
</head>
<body>
<h1>My App</h1>
<!-- Regular script (old way) -->
<script src="old-script.js"></script>
<!-- Module script (modern way) -->
<script type="module" src="main.js"></script>
<!-- Inline module -->
<script type="module">
import { add } from './math.js';
console.log(add(5, 3));
</script>
</body>
</html>Key Takeaways
- Modules organize code into separate files with explicit dependencies
exportmakes functionality available to other modulesimportbrings functionality from other modules- Named exports: multiple per file, exact names required
- Default export: one per file, any name on import
import * as nameimports all as an object- Use aliases with 'as' to rename imports/exports
- Dynamic imports with import() for code splitting
- Modules have their own scope (no global pollution)
- Organize by feature: utils/, models/, services/, components/
What's Next?
You now understand JavaScript modules and can organize code into maintainable, reusable pieces! You've learned named and default exports, import patterns, and how to structure modular applications.
In the next lesson, we'll explore Optional Chaining and Nullish Coalescing—two powerful operators (?. and ??) that make working with potentially null/undefined values much cleaner and safer!
💪 Practice Challenge:
Before moving on, try:
- Create a math utilities module with named exports
- Create a User class module with default export
- Build an API service module with multiple functions
- Organize a project into utils/, models/, services/ folders
- Practice importing with aliases and namespace imports