JavaScript Modules: Import and Export Explained
The Problem: Life Before Modules
Imagine you're building a web application. You start with a single JavaScript file, and everything works fine. But as your project grows, you add more features, more functions, and suddenly your app.js file looks like this:
// app.js - A 2000+ line nightmare
function calculateTax(price) {
return price * 0.18;
}
function formatCurrency(amount) {
return `$${amount.toFixed(2)}`;
}
function validateEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
function validatePhone(phone) {
return /^\d{10}$/.test(phone);
}
function getUserData() {
// 50 lines of code
}
function processPayment() {
// 100 lines of code
}
// ... 50 more functions
// ... variable declarations everywhere
// ... potential naming conflicts
Problems with this approach:
Hard to maintain: Finding a specific function in a 2000-line file is like finding a needle in a haystack
Name collisions: If you accidentally declare
calculateTax()twice, the second one overwrites the firstNo clear organization: Related functions are scattered everywhere
Difficult testing: You can't easily test individual pieces in isolation
Team collaboration nightmare: Multiple developers editing the same massive file leads to merge conflicts
No code reusability: Can't easily reuse functions in other projects
The Solution: JavaScript Modules
Modules allow you to split your code into separate files, where each file is responsible for a specific feature or functionality. Think of it like organizing a library—instead of having all books in one giant pile, you organize them into sections.
Before Modules: After Modules:
┌─────────────────┐ ┌──────────────┐
│ │ │ app.js │
│ │ │ (main file) │
│ app.js │ └──────┬───────┘
│ (2000 lines) │ │
│ │ ┌──────┴───────────────────┐
│ - taxes │ │ │
│ - validation │ ┌────▼─────┐ ┌────▼──────┐ ▼
│ - formatting │ │ taxes.js │ │ format.js │ ...
│ - api calls │ └──────────┘ └───────────┘
│ - everything! │ (50 lines) (30 lines)
│ │
└─────────────────┘
Understanding Exports: Sharing Code
When you create a module, you need to export the functions, objects, or values you want to make available to other files.
Named Exports
Named exports allow you to export multiple things from a single module. Each export has a specific name.
// mathUtils.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 const PI = 3.14159;
Alternative syntax (export at the end):
// mathUtils.js
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
const PI = 3.14159;
// Export everything at once
export { add, subtract, PI };
Default Exports
Each module can have one default export. Use this when your module has a single primary functionality.
// logger.js
export default function log(message) {
console.log(`[LOG]: ${message}`);
}
Or with classes:
// User.js
export default class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
greet() {
return `Hello, I'm ${this.name}`;
}
}
Understanding Imports: Using Code from Other Modules
Once you've exported code from a module, you need to import it to use it elsewhere.
Importing Named Exports
When importing named exports, you must use the exact same names and wrap them in curly braces {}:
// app.js
import { add, subtract, PI } from './mathUtils.js';
console.log(add(5, 3)); // 8
console.log(subtract(10, 4)); // 6
console.log(PI); // 3.14159
Import specific items only:
import { add } from './mathUtils.js';
// Only 'add' is available, not subtract or PI
Import with aliases (renaming):
import { add as sum, subtract as minus } from './mathUtils.js';
console.log(sum(5, 3)); // 8
console.log(minus(10, 4)); // 6
Import everything as a namespace:
import * as MathUtils from './mathUtils.js';
console.log(MathUtils.add(5, 3)); // 8
console.log(MathUtils.subtract(10, 4)); // 6
console.log(MathUtils.PI); // 3.14159
Importing Default Exports
With default exports, you can choose any name when importing (no curly braces needed):
// app.js
import log from './logger.js';
// Could also name it: import logger from './logger.js';
log('Application started'); // [LOG]: Application started
// main.js
import User from './User.js';
const user = new User('Alice', 'alice@example.com');
console.log(user.greet()); // Hello, I'm Alice
Mixing Default and Named Exports
You can have both in the same module:
// utils.js
// Default export
export default function formatDate(date) {
return date.toLocaleDateString();
}
// Named exports
export function formatTime(date) {
return date.toLocaleTimeString();
}
export function formatDateTime(date) {
return date.toLocaleString();
}
Importing both:
// app.js
import formatDate, { formatTime, formatDateTime } from './utils.js';
const now = new Date();
console.log(formatDate(now)); // 3/24/2026
console.log(formatTime(now)); // 10:30:45 AM
console.log(formatDateTime(now)); // 3/24/2026, 10:30:45 AM
Practical Example: Building a Utility Library
Let's organize a small utility library using modules:
Project Structure
my-app/
├── index.html
├── app.js (main file)
└── utils/
├── validators.js
├── formatters.js
└── calculations.js
File 1: validators.js
// utils/validators.js
export function validateEmail(email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
export function validatePhone(phone) {
const regex = /^\d{10}$/;
return regex.test(phone);
}
export function validatePassword(password) {
// At least 8 characters, 1 uppercase, 1 lowercase, 1 number
return password.length >= 8 &&
/[A-Z]/.test(password) &&
/[a-z]/.test(password) &&
/[0-9]/.test(password);
}
File 2: formatters.js
// utils/formatters.js
export function formatCurrency(amount) {
return `$${amount.toFixed(2)}`;
}
export function formatPercentage(value) {
return `${(value * 100).toFixed(1)}%`;
}
export default function capitalizeFirstLetter(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
File 3: calculations.js
// utils/calculations.js
export function calculateTax(price, taxRate = 0.18) {
return price * taxRate;
}
export function calculateDiscount(price, discountPercent) {
return price * (discountPercent / 100);
}
export function calculateTotal(price, taxRate = 0.18) {
const tax = calculateTax(price, taxRate);
return price + tax;
}
File 4: app.js (Main Application)
// app.js
import { validateEmail, validatePassword } from './utils/validators.js';
import { formatCurrency, formatPercentage } from './utils/formatters.js';
import capitalize from './utils/formatters.js'; // default import
import * as Calc from './utils/calculations.js';
// Using validators
const email = 'user@example.com';
if (validateEmail(email)) {
console.log('✓ Valid email');
}
const password = 'SecurePass123';
if (validatePassword(password)) {
console.log('✓ Strong password');
}
// Using formatters
const price = 99.99;
console.log(formatCurrency(price)); // $99.99
console.log(formatPercentage(0.15)); // 15.0%
console.log(capitalize('hello world')); // Hello world
// Using calculations
const tax = Calc.calculateTax(price);
const total = Calc.calculateTotal(price);
console.log(`Price: ${formatCurrency(price)}`);
console.log(`Tax: ${formatCurrency(tax)}`);
console.log(`Total: ${formatCurrency(total)}`);
Module Dependency Diagram
app.js
│
┌─────────────┼─────────────┐
│ │ │
▼ ▼ ▼
validators.js formatters.js calculations.js
(Each module is independent and reusable)
Default vs Named Exports: When to Use Which?
Use Named Exports When:
✅ Your module contains multiple related utilities
✅ You want explicit naming (reduces confusion)
✅ You're creating a library with multiple functions
// Good for utility modules
export { validateEmail, validatePhone, validatePassword };
Use Default Exports When:
✅ Your module has a single main purpose
✅ You're exporting a class
✅ You're exporting a single configuration object
// Good for classes or single-purpose modules
export default class Database { }
Comparison Table
| Feature | Named Exports | Default Exports |
|---|---|---|
| Number per module | Multiple | One per module |
| Import syntax | import { name } from './file.js' |
import name from './file.js' |
| Renaming on import | import { name as alias } |
Automatically any name |
| Clarity | More explicit | Can be ambiguous |
| Best for | Utility functions, constants | Classes, main components |
Benefits of Modular Code
1. Better Organization
Instead of this: You get this:
app.js (2000 lines) ├── app.js (50 lines)
├── auth/
│ ├── login.js
│ └── register.js
├── utils/
│ ├── validators.js
│ └── formatters.js
└── api/
└── users.js
2. Code Reusability
Write once, use everywhere:
// Use in Project A
import { validateEmail } from './utils/validators.js';
// Use in Project B (copy the validators.js file)
import { validateEmail } from './utils/validators.js';
// Use in Project C
import { validateEmail } from './utils/validators.js';
3. Easier Testing
Test individual modules in isolation:
// validators.test.js
import { validateEmail } from './validators.js';
test('validates correct email', () => {
expect(validateEmail('test@example.com')).toBe(true);
});
test('rejects invalid email', () => {
expect(validateEmail('invalid-email')).toBe(false);
});
4. Better Collaboration
Team members can work on different modules simultaneously:
Developer A works on
auth.jsDeveloper B works on
validators.jsDeveloper C works on
api.js
No merge conflicts! 🎉
5. Maintainability
Finding and fixing bugs becomes easier:
Bug: Email validation not working
Fix location: utils/validators.js (30 lines)
Instead of: app.js (2000 lines)
6. Lazy Loading (Performance)
Load only what you need:
// Load expensive modules only when needed
button.addEventListener('click', async () => {
const { heavyFunction } = await import('./heavy-module.js');
heavyFunction();
});
Module Import/Export Flow
┌─────────────────────┐
│ module.js │
│ │
│ function helper() │
│ │
│ export { helper } │◄─── 1. Export makes code available
└──────────┬──────────┘
│
│ 2. File system link
│
┌──────────▼──────────┐
│ app.js │
│ │
│ import { helper } │◄─── 3. Import brings code in
│ from './module.js' │
│ │
│ helper(); │◄─── 4. Use the imported function
└─────────────────────┘
A Quick Note on CommonJS (Node.js)
Before ES6 modules became standard, Node.js used CommonJS syntax. You might see this in older codebases:
CommonJS Syntax
// Exporting (CommonJS)
function add(a, b) {
return a + b;
}
module.exports = { add };
// Importing (CommonJS)
const { add } = require('./math.js');
ES6 Modules vs CommonJS
| Feature | ES6 Modules | CommonJS |
|---|---|---|
| Syntax | import/export |
require/module.exports |
| Browser support | ✅ Yes (modern browsers) | ❌ No (needs bundler) |
| Node.js support | ✅ Yes (Node 12+) | ✅ Yes |
| Static analysis | ✅ Yes | ❌ No |
| Tree shaking | ✅ Yes | ❌ Limited |
Recommendation: Use ES6 modules (import/export) for new projects. They're the modern standard and work everywhere.
Using Modules in HTML
To use ES6 modules in the browser, add type="module" to your script tag:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My App</title>
</head>
<body>
<h1>JavaScript Modules Demo</h1>
<!-- Add type="module" -->
<script type="module" src="app.js"></script>
</body>
</html>
Important notes:
Modules are automatically in strict mode
Modules have their own scope (no global pollution)
Modules are deferred by default (load after HTML parsing)
Modules can only be loaded via HTTP(S), not
file://protocol (use a local server)
Common Mistakes to Avoid
❌ Mistake 1: Forgetting file extensions
// Wrong
import { add } from './math';
// Correct
import { add } from './math.js';
❌ Mistake 2: Mixing up default and named imports
// validators.js
export function validateEmail(email) { }
// Wrong - using default import for named export
import validateEmail from './validators.js'; // undefined!
// Correct
import { validateEmail } from './validators.js';
❌ Mistake 3: Circular dependencies
// a.js
import { b } from './b.js';
export const a = b + 1;
// b.js
import { a } from './a.js'; // ⚠️ Circular dependency!
export const b = a + 1;
❌ Mistake 4: Forgetting type="module" in HTML
<!-- Wrong - modules won't work -->
<script src="app.js"></script>
<!-- Correct -->
<script type="module" src="app.js"></script>
Best Practices
✅ 1. One Module, One Responsibility
Each module should do one thing well:
// Good ✓
// validators.js - only validation
// formatters.js - only formatting
// calculations.js - only calculations
// Bad ✗
// utils.js - validation, formatting, calculations, API calls, everything!
✅ 2. Use Clear, Descriptive Names
// Good ✓
import { validateEmail } from './validators.js';
import { formatCurrency } from './formatters.js';
// Bad ✗
import { vE } from './v.js';
import { fC } from './f.js';
✅ 3. Group Related Imports
// Good ✓
import React from 'react';
import ReactDOM from 'react-dom';
import { validateEmail, validatePassword } from './utils/validators.js';
import { formatCurrency } from './utils/formatters.js';
import './styles.css';
✅ 4. Prefer Named Exports for Utilities
// Good ✓ - Clear what you're importing
import { validateEmail, validatePhone } from './validators.js';
// Less ideal - You have to check the file to know what it exports
import validators from './validators.js';
✅ 5. Keep Modules Small
Aim for modules under 200-300 lines. If it's getting bigger, split it up!
Summary
Modules solve the problem of organizing large JavaScript applications by splitting code into smaller, manageable pieces.
Key Concepts:
Export to share code from a module
Named exports:
export { name }Default exports:
export default value
Import to use code from other modules
Named imports:
import { name } from './file.js'Default imports:
import name from './file.js'
Benefits:
Better organization
Code reusability
Easier testing
Better collaboration
Improved maintainability
Quick Reference:
// Export
export function myFunction() { } // Named
export default function() { } // Default
export { func1, func2 }; // Multiple named
// Import
import { myFunction } from './file.js'; // Named
import myDefault from './file.js'; // Default
import * as All from './file.js'; // Everything
import def, { named } from './file.js'; // Both
What's Next?
Now that you understand modules, you can:
Organize your projects more effectively
Build reusable utility libraries
Collaborate better with your team
Write cleaner, more maintainable code
Start small—take one large file and split it into modules. You'll immediately see the benefits!
Happy coding! 🚀