
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.
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.
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. -->
javascript// 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.
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.
You can export any number of things from a single file by naming them:
javascript// 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:
javascript// 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.
Each file can have one default export. It's meant to represent the primary thing a module provides:
javascript// logger.js function log(message) { console.log(`[LOG]: ${message}`); } export default log;
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.
javascript// 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.
javascript// 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:
javascriptimport myLogger from "./logger.js"; // totally valid
If you need multiple exports from a file and don't want to list them all, you can namespace the import:
javascriptimport * 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.
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.
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:
javascript// 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.
Sometimes names clash, or you just want a cleaner alias:
javascriptimport { 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:
javascriptexport { add as sum, subtract as minus };
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.
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.
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.
javascript// 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.
Modules are one of those foundational concepts that quietly make everything else possible. Here's the short version:
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.
Related posts based on tags, category, and projects
Promises are one of those JavaScript concepts that sound intimidating but click instantly once you see the right mental model. This post breaks them down from scratch, covering why they exist, how they work, and how to use them properly.
Promises are the backbone of asynchronous JavaScript, and most developers use them daily without truly understanding how they work under the hood. This guide breaks down everything from the basics to internal mechanics, all six static methods, common pitfalls, and best practices, with analogies pulled straight from Marvel, DC, and anime.
Destructuring is a concise syntax for pulling values out of arrays and objects into individual variables. This post covers array destructuring, object destructuring, default values, and the real-world patterns where this shines.
Spread and rest both use `...` but do opposite things. Spread expands an iterable into individual elements. Rest collects individual elements into a single array. This post breaks down each one clearly with real examples.