Skip to main content

Command Palette

Search for a command to run...

JavaScript Modules: Import and Export Explained

Updated
12 min read

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:

  1. Hard to maintain: Finding a specific function in a 2000-line file is like finding a needle in a haystack

  2. Name collisions: If you accidentally declare calculateTax() twice, the second one overwrites the first

  3. No clear organization: Related functions are scattered everywhere

  4. Difficult testing: You can't easily test individual pieces in isolation

  5. Team collaboration nightmare: Multiple developers editing the same massive file leads to merge conflicts

  6. 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.js

  • Developer B works on validators.js

  • Developer 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';
// 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:

  1. Export to share code from a module

    • Named exports: export { name }

    • Default exports: export default value

  2. Import to use code from other modules

    • Named imports: import { name } from './file.js'

    • Default imports: import name from './file.js'

  3. 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! 🚀