Skip to content
Back to blogs
May 6, 2026·Updated May 6, 2026·7 min read

JavaScript Modules: Import and Export Explained

Learn how JavaScript modules work, the difference between default and named exports, and why modular code makes your projects easier to maintain and scale.

Summary

JavaScript modules let you split your code into focused, reusable files instead of one giant mess. This post walks through why that matters, how exports and imports work, and when to use default vs named exports.

JavaScript Modules: Import and Export Explained

Think of your codebase as a kitchen. In a small restaurant, one cook can handle everything: prep, cooking, plating. But scale up and suddenly that same person is dropping plates, burning orders, and blocking everyone else. You split the work: one person on prep, one on grill, one on sauces. Each role is focused, independent, and easy to swap out. JavaScript modules work the same way. Each file has a clear job, and the rest of the codebase just asks for what it needs.

The Problem: Life Without Modules

Back when JavaScript only ran in browsers for simple interactions, dumping everything in one file was fine. Then applications grew. Suddenly you had a 3000-line app.js with utility functions tangled up next to API calls tangled up next to UI logic. You'd scroll forever to find a bug. Changing one thing would break something three screens away. And sharing code between files? That meant global variables, which meant unpredictable side effects.

Here's what that looked like:

<!-- index.html -->
<script src="app.js"></script>
<!-- 3000 lines. Good luck. -->
// app.js - the wild west
var userName = "Atharv";
var API_URL = "https://api.example.com";
 
function fetchUser() { ... }
function renderUser() { ... }
function validateEmail() { ... }
function calculateTax() { ... }
// 2950 more lines below...

Everything is globally scoped. Any file can accidentally overwrite userName. Nothing is encapsulated. There's no clear contract between parts of the system.

┌──────────────────────────────────────┐
│              app.js                  │
│                                      │
│  fetchUser()   renderUser()          │
│  validateEmail()  calculateTax()     │
│  userName  API_URL  taxRate          │
│  formatDate()  sendAnalytics()       │
│                                      │
│  Everything tangled, nothing clear   │
└──────────────────────────────────────┘

Modules solve this. One file, one responsibility. You declare what you share, and everything else stays private.

Exporting: Declaring What You Share

A module is just a JavaScript file. By default, everything inside it is private. If you want something to be usable from the outside, you export it.

There are two ways to export: named exports and a default export.

Named Exports

You can export any number of things from a single file by naming them:

// math.js
export function add(a, b) {
  return a + b;
}
 
export function subtract(a, b) {
  return a - b;
}
 
export const PI = 3.14159;

You can also export at the bottom, which many find cleaner because you see the full module at a glance before deciding what's public:

// math.js
function add(a, b) {
  return a + b;
}
 
function subtract(a, b) {
  return a - b;
}
 
const PI = 3.14159;
 
export { add, subtract, PI };

Both are equivalent. The second form is easier to scan for what's actually exported, especially in larger files.

Default Export

Each file can have one default export. It's meant to represent the primary thing a module provides:

// logger.js
function log(message) {
  console.log(`[LOG]: ${message}`);
}
 
export default log;

Importing: Consuming What Others Share

Once something is exported, you pull it in with import. The syntax differs depending on whether you're importing a named export or a default.

Importing Named Exports

// main.js
import { add, subtract, PI } from "./math.js";
 
console.log(add(2, 3)); // 5
console.log(subtract(10, 4)); // 6
console.log(PI); // 3.14159

The curly braces are required here. They tell JavaScript exactly which named exports to pull in. You're not importing the whole file; you're cherry-picking.

Importing a Default Export

// main.js
import log from "./logger.js";
 
log("App started"); // [LOG]: App started

No curly braces. You also get to name it whatever you want since there's no "name" to match:

import myLogger from "./logger.js"; // totally valid

Importing Everything at Once

If you need multiple exports from a file and don't want to list them all, you can namespace the import:

import * as MathUtils from "./math.js";
 
MathUtils.add(1, 2); // 3
MathUtils.PI; // 3.14159

This is useful, but don't overuse it. Explicit imports make it clear what a file actually depends on.

The Import/Export Flow

Here's how a realistic project structure connects through modules:

┌─────────────┐       ┌─────────────┐
│  math.js    │       │  logger.js  │
│             │       │             │
│ export add  │       │ export log  │
│ export PI   │       │  (default)  │
└──────┬──────┘       └──────┬──────┘
       │                     │
       │  import { add, PI } │  import log
       └──────────┬──────────┘
                  │
          ┌───────▼───────┐
          │    main.js    │
          │               │
          │  add(2, 3)    │
          │  log("hello") │
          └───────────────┘

Each file is a node. It declares its dependencies explicitly through imports, and its contributions explicitly through exports. Nothing is hidden, nothing is global.

Default vs Named Exports: When to Use Which

This is where most beginners get confused, so let's be direct about it.

┌─────────────────────────────────────────────────────┐
│              Default Export                         │
│                                                     │
│  export default function Button() { ... }           │
│                                                     │
│  - One per file                                     │
│  - Represents the "main thing" the file provides    │
│  - Importer can rename it freely                    │
│  - Common for React components, classes             │
└─────────────────────────────────────────────────────┘
 
┌─────────────────────────────────────────────────────┐
│              Named Exports                          │
│                                                     │
│  export function add() { ... }                      │
│  export function subtract() { ... }                 │
│                                                     │
│  - Multiple per file                                │
│  - Each has a specific name                         │
│  - Importer must use that name (or alias it)        │
│  - Common for utility functions, constants          │
└─────────────────────────────────────────────────────┘

Use default exports when a file revolves around one primary thing: a component, a class, a factory function. Use named exports when a file is a collection of related utilities.

You can also mix them, though it can get confusing:

// api.js
export const BASE_URL = "https://api.example.com";
 
export function get(endpoint) { ... }
export function post(endpoint, body) { ... }
 
export default {
  get,
  post,
  BASE_URL
};

In practice, many teams pick one style and stick to it. Named exports are generally preferred in utility files because they're refactoring-friendly: your editor can auto-import them by name, and unused exports are easier to detect.

Renaming Imports

Sometimes names clash, or you just want a cleaner alias:

import { add as sum, subtract as minus } from "./math.js";
 
sum(1, 2); // 3
minus(5, 2); // 3

Works the same way on the export side too:

export { add as sum, subtract as minus };

A Real File Dependency Diagram

Here's what a small app with proper modules looks like:

src/
├── utils/
│   ├── math.js          ← exports add, subtract, PI
│   └── format.js        ← exports formatDate, formatCurrency
├── services/
│   └── api.js           ← imports from utils/format.js
│                           exports fetchUser, fetchPosts
├── components/
│   └── UserCard.js      ← imports from services/api.js
│                           default export: UserCard component
└── main.js              ← imports UserCard, kicks everything off
 
        math.js ──────────────────────────┐
        format.js ──── api.js ─── UserCard.js ─── main.js

Every dependency is traceable. If fetchUser breaks, you know exactly where to look. If you need to change how dates are formatted, you change one file and every consumer gets the update.

Benefits of Modular Code

Here's the thing: the benefits aren't just theoretical. They show up in real work.

Maintainability. Small files with a single responsibility are easier to reason about. You can read a 80-line utility file and fully understand it. You can't do that with a 3000-line monolith.

Reusability. Once math.js is written and tested, any file in the project can use it. No copy-pasting functions around.

Encapsulation. Anything you don't export stays private. That means you can refactor internals freely without worrying about breaking consumers. The exported API is your contract, and everything else is an implementation detail.

Testability. Isolated modules are easy to unit test. Import the function, give it inputs, assert outputs. No global state to set up, no side effects bleeding in.

Tree-shaking. Bundlers like Vite and Webpack can analyze your imports and exclude code that's never used. If you only import add from math.js, subtract doesn't end up in your final bundle. Named exports make this especially effective.

A Note on Module Types

You'll run into two module systems in JavaScript. ES Modules (ESM) is the standard, the one we've been using throughout this post, with import/export syntax. CommonJS (CJS) is the older Node.js format using require() and module.exports.

// CommonJS (older Node.js style)
const { add } = require("./math");
module.exports = { add, subtract };
 
// ES Modules (modern standard)
import { add } from "./math.js";
export { add, subtract };

Modern Node.js supports both. For new projects, prefer ESM. It's the standard, browsers understand it natively, and it's what the entire modern tooling ecosystem is built around.

Key Takeaways

Modules are one of those foundational concepts that quietly make everything else possible. Here's the short version:

  • Without modules, code becomes an unmanageable global soup.
  • Named exports let you share multiple things from one file. Default exports represent the main thing a file provides.
  • Import only what you need. Explicit dependencies are readable dependencies.
  • Modules enable encapsulation, reusability, testability, and better tooling support.
  • ES Modules is the standard. Use import/export, not require.

Once you start thinking in modules, the instinct to keep files small and focused becomes automatic. That's the point where code organization stops being a chore and starts feeling natural.