Skip to main content

Command Palette

Search for a command to run...

JavaScript Modules: Import and Export Explained

Updated
9 min read
JavaScript Modules: Import and Export Explained
H
CS undergrad | Tech enthusiast | Focusing on Web Dev • DSA • ML | Building skills for real-world impact

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 export

  • Default exports represent the primary thing a module does using export default

  • Import 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)