JavaScript Modules: Import and Export Explained

As your JavaScript projects grow, keeping all code in a single file becomes a nightmare. Functions pile up, variables clash, and it becomes impossible to find anything. Modules solve this by letting you split code into separate files, each responsible for one thing.
In this article we will cover how to organize JavaScript using modules, understand exports and imports, and learn the difference between default and named exports.
Why Modules Are Needed
The Problem: Everything in One File
Imagine a project that grows to 5000 lines in a single app.js file:
// app.js — 5000 lines of chaos
function calculatePrice(items) {
return items.reduce((sum, item) => sum + item.price, 0);
}
function formatCurrency(amount) {
return "$" + amount.toFixed(2);
}
function sendEmail(to, subject, body) {
// Email logic here
}
function validateForm(data) {
// Validation logic here
}
function connectDatabase(uri) {
// Database logic here
}
// ... 4900 more lines
Problems:
- Hard to find what you need in 5000 lines
- Global namespace pollution — all functions are global
- Name collisions — someone else might have a
formatDate()function - No clear responsibility — which functions belong together?
- Difficult to test — can't test one feature in isolation
- Can't reuse code across projects easily
The Solution: Split Into Modules
project/
├── utils/
│ ├── price.js (price calculation)
│ └── currency.js (currency formatting)
├── email/
│ └── sender.js (email logic)
├── validation/
│ └── form.js (form validation)
├── database/
│ └── connection.js (database logic)
└── app.js (brings it all together)
Now each file has a clear purpose. Code is organized, reusable, and testable.
Exporting Functions or Values
Named Exports
Use export to make functions, variables, or classes available to other files.
utils/price.js:
export function calculatePrice(items) {
return items.reduce((sum, item) => sum + item.price, 0);
}
export function applyDiscount(price, discount) {
return price * (1 - discount);
}
export const TAX_RATE = 0.1;
You can export multiple things from one file. Each export gets a name.
Default Exports
A default export is the main thing a module does. Each module can have only one default export.
utils/currency.js:
export default function formatCurrency(amount) {
return "$" + amount.toFixed(2);
}
Default exports are useful when a module has one primary purpose.
Mixing Default and Named Exports
You can have one default and multiple named exports in the same file:
email/sender.js:
export default function sendEmail(to, subject, body) {
console.log(`Email sent to \({to}: \){subject}`);
}
export function sendBulkEmail(recipients, subject, body) {
recipients.forEach(to => sendEmail(to, subject, body));
}
export const EMAIL_TEMPLATES = {
welcome: "Welcome to our app!",
reset: "Click here to reset your password"
};
Importing Modules
Importing Named Exports
Use curly braces {} to import specific named exports:
app.js:
import { calculatePrice, applyDiscount, TAX_RATE } from "./utils/price.js";
const items = [
{ name: "Apple", price: 2 },
{ name: "Banana", price: 1.5 }
];
let total = calculatePrice(items);
console.log("Total:", total); // 3.5
let discounted = applyDiscount(total, 0.2);
console.log("After 20% discount:", discounted); // 2.8
console.log("Tax rate:", TAX_RATE); // 0.1
The names in the curly braces must match the exports exactly.
Importing Default Exports
Default exports can be imported with any name:
app.js:
import formatCurrency from "./utils/currency.js";
let total = 99.99;
console.log(formatCurrency(total)); // "$99.99"
Notice no curly braces. The name formatCurrency could be anything — it matches the default export regardless of what the file calls it internally.
Importing Both Default and Named
import sendEmail, { sendBulkEmail, EMAIL_TEMPLATES } from "./email/sender.js";
sendEmail("john@example.com", "Hello", "How are you?");
sendBulkEmail(["alice@example.com", "bob@example.com"], "Update", "Check this out");
console.log(EMAIL_TEMPLATES.welcome);
Renaming Imports
If two modules export something with the same name, rename on import:
import { formatCurrency as formatUSD } from "./utils/usd.js";
import { formatCurrency as formatEUR } from "./utils/eur.js";
console.log(formatUSD(100)); // $100.00
console.log(formatEUR(100)); // €100,00
Default vs Named Exports
| Aspect | Default Export | Named Export |
|---|---|---|
| Count per file | 1 maximum | Multiple |
| Syntax | export default |
export |
| Import syntax | No braces | Curly braces |
| Renamed on import | Always | Optionally |
| Use case | Primary responsibility | Utilities and helpers |
When to Use Each
Use default export when:
- The module has one clear purpose
- Someone will likely want the main thing it exports
// logger.js — one job: create a logger
export default function createLogger(name) {
return {
log: (msg) => console.log(`[\({name}] \){msg}`)
};
}
Use named exports when:
- The module provides multiple related utilities
- Users might want some exports but not others
// math.js — provides multiple math functions
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
export function multiply(a, b) { return a * b; }
Module Import/Export Flow
How modules work together:
price.js
|
| exports: calculatePrice, applyDiscount, TAX_RATE
|
app.js ----> imports: { calculatePrice, TAX_RATE }
|
├---> uses calculatePrice()
|
└---> uses TAX_RATE
currency.js
|
| exports default: formatCurrency
|
app.js ----> imports: formatCurrency
|
└---> uses formatCurrency()
Each file declares what it exports. Other files import only what they need.
File Dependency Diagram
A larger project structure:
┌─── database/connection.js
│
├─── email/sender.js
│
├─── utils/price.js
│
├─── utils/currency.js
│
└─── validation/form.js
|
| (all imported by)
v
app.js (main file)
app.js is the entry point. It imports from other modules. Those modules are independent and can be tested separately.
Benefits of Modular Code
1. Readability
Each file is focused. You open price.js and see only pricing logic:
// price.js — 50 lines, clear purpose
export function calculatePrice(items) { ... }
export function applyDiscount(price, discount) { ... }
Much better than scrolling through 5000 lines.
2. Maintainability
Change pricing logic? Edit one file. No risk of breaking unrelated code.
// Only price.js changes
export function calculatePrice(items) {
// New logic here
return items.reduce((sum, item) => sum + item.price * 1.05, 0);
}
3. Testability
Test each module independently:
import { calculatePrice, applyDiscount } from "./utils/price.js";
// Test calculatePrice
const items = [{ price: 100 }];
console.assert(calculatePrice(items) === 100, "Price calculation failed");
// Test applyDiscount
console.assert(applyDiscount(100, 0.2) === 80, "Discount failed");
4. Reusability
Use the same module across multiple projects:
// Project A: E-commerce app
import { calculatePrice } from "../shared/utils/price.js";
// Project B: Billing system
import { calculatePrice } from "../shared/utils/price.js";
Both projects share the same tested, reliable code.
5. Team Collaboration
Multiple developers work on different modules without stepping on each other:
Alice works on: email/sender.js
Bob works on: validation/form.js
Charlie works on: database/connection.js
All three changes merge without conflict
6. Namespace Control
No global variable pollution. Each module has its own scope:
// module-a.js
let config = { ... }; // This config is private to module-a
// module-b.js
let config = { ... }; // Different config, no collision
Complete Real-World Example
Project Structure
project/
├── utils/
│ ├── calculator.js
│ └── formatter.js
├── services/
│ └── api.js
└── app.js
utils/calculator.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;
}
utils/formatter.js:
export default function formatResult(number) {
return `Result: ${number}`;
}
services/api.js:
export default async function fetchData(url) {
const response = await fetch(url);
return response.json();
}
export function buildURL(base, endpoint) {
return `\({base}/\){endpoint}`;
}
app.js (brings it all together):
import { add, subtract, multiply } from "./utils/calculator.js";
import formatResult from "./utils/formatter.js";
import fetchData, { buildURL } from "./services/api.js";
// Use calculator functions
const sum = add(10, 5);
console.log(formatResult(sum)); // Result: 15
const product = multiply(3, 4);
console.log(formatResult(product)); // Result: 12
// Use API utilities
const url = buildURL("https://api.example.com", "users");
console.log("Fetching from:", url);
fetchData(url).then(data => console.log("Data:", data));
Clear separation of concerns. Each module has one job.
Importing All Exports
If a module has many named exports, import them all at once:
import * as calculator from "./utils/calculator.js";
calculator.add(10, 5); // 15
calculator.subtract(10, 5); // 5
calculator.multiply(10, 5); // 50
All exports become properties of the calculator object.
Practice Assignment
Build a simple modular project:
1. Create utils/string.js with string utilities:
export function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
export function reverse(str) {
return str.split("").reverse().join("");
}
export function countWords(str) {
return str.split(" ").length;
}
2. Create utils/array.js with array utilities:
export default function findMax(arr) {
return Math.max(...arr);
}
export function findMin(arr) {
return Math.min(...arr);
}
export function average(arr) {
return arr.reduce((a, b) => a + b, 0) / arr.length;
}
3. Create app.js that imports and uses both:
import { capitalize, reverse, countWords } from "./utils/string.js";
import findMax, { findMin, average } from "./utils/array.js";
console.log(capitalize("hello")); // Hello
console.log(reverse("hello")); // olleh
console.log(countWords("hello world test")); // 3
const numbers = [5, 2, 8, 1, 9];
console.log(findMax(numbers)); // 9
console.log(findMin(numbers)); // 1
console.log(average(numbers)); // 5
Try running this in Node.js (add "type": "module" to your package.json).
Quick Recap
Modules let you split code into separate files, each with a clear purpose
Code in one file doesn't automatically pollute the global namespace
Named exports let you export multiple things from a file using
exportDefault exports represent the primary thing a module does using
export defaultImport named exports with curly braces:
import { name } from "./file.js"Import default exports without braces:
import name from "./file.js"You can mix default and named exports in the same file
Rename imports with
as:import { price as cost } from "./file.js"Modules improve readability, testability, maintainability, and code reuse
Each module has its own scope — no global variable collisions
Use default exports for a module's primary responsibility
Use named exports for related utilities and helpers
Modular code is the foundation of scalable JavaScript projects. Once you organize code into modules, large projects become manageable.
Happy coding! 🚀
If you enjoyed this article, check out my other blogs on this profile. 🔗 Connect with me: LinkedIn | GitHub | X (Twitter)




