Understanding JavaScript Modules

From tangled global scripts to clean, maintainable code β a complete guide to ES modules, exports, and imports.
1. Why Modules? The Problem First
Before modules existed, every JavaScript file you loaded in a browser shared one big global scope. Every variable, every function β all of them lived together in the same room. That causes a predictable mess.
app.js & helpers.js (the old way)
// helpers.js β loaded first via <script>
var MAX_RETRIES = 3;
function formatDate(d) { return d.toString(); }
// app.js β loaded second via <script>
var MAX_RETRIES = 10; // β Silently overwrites helpers.js
formatDate = "oops"; // β Anyone can mutate anything
// And the dreaded script-order dependency:
// <script src="app.js"></script> β crashes, formatDate not yet defined
// <script src="helpers.js"></script>
As your codebase grows, this becomes a nightmare. There's no way to tell what depends on what. Rename one function and something unrelated breaks. Add a new library that uses the same global name as yours and everything explodes at runtime.
The core problem:
Without encapsulation, code is impossible to reason about in isolation. Every file can read and modify every other file's variables.
Modules solve this by giving each file its own scope. Nothing leaks out unless you explicitly export it, and nothing leaks in unless you explicitly import it.
2. Exporting β Sharing What You Build
A module keeps everything private by default. To make something available to other files, you use the export keyword. There are two flavours: named exports and the default export.
Named Exports
You can export as many things as you want from a single file by placing export in front of declarations. The names you export become the "public API" of that module.
math.js
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
export const PI = 3.14159;
// This is NOT exported β it stays private to math.js
function _internalHelper() { /* ... */ }
You can also group exports at the bottom of the file, which many teams prefer for readability:
math.js (alternative style)
function add(a, b) { return a + b; }
function subtract(a, b) { return a - b; }
const PI = 3.14159;
// All exports listed in one place β easy to scan
export { add, subtract, PI };
Default Export
Each module can have at most one default export. This is meant for the primary thing a module provides β a class, a main function, a config object.
logger.js
function log(message, level = 'info') {
const time = new Date().toLocaleTimeString();
console.log(`[\({level.toUpperCase()}] \){time}: ${message}`);
}
export default log; // The "main thing" this file provides
Tip: Think of named exports as a toolbox (multiple tools), and default exports as the one main item in a package.
3. Importing β Using What Others Built
The import statement always goes at the top of a file. It declaratively lists exactly what your file needs β making dependencies obvious at a glance.
Importing Named Exports
Use curly braces to pick specific exports by their exact name.
app.js
import { add, subtract, PI } from './math.js';
console.log(add(10, 5)); // 15
console.log(subtract(10, 5)); // 5
console.log(PI); // 3.14159
Importing the Default Export
No curly braces needed. You can name it anything β that's one of the key traits of default imports.
app.js
import log from './logger.js'; // No curly braces
import myLogger from './logger.js'; // This also works β any name
log('Server started'); // [INFO] 10:32:01: Server started
log('File not found', 'error'); // [ERROR] 10:32:01: File not found
Renaming & Namespace Imports
If two modules export the same name, use as to rename on import. You can also import everything at once into an object with * as.
app.js
// Rename to avoid a naming conflict
import { add as mathAdd } from './math.js';
import { add as arrayAdd } from './array-utils.js';
// Import everything into a namespace object
import * as MathUtils from './math.js';
console.log(MathUtils.add(2, 3)); // 5
console.log(MathUtils.PI); // 3.14159
Using Modules in the Browser
To enable ES modules in an HTML file, add type="module" to your script tag. This one attribute unlocks strict mode, top-level await, and import/export syntax.
index.html
<!-- Old way: no isolation, order-dependent -->
<script src="helpers.js"></script>
<script src="app.js"></script>
<!-- New way: type="module" enables ES module syntax -->
<script type="module" src="app.js"></script>
The diagram above shows a realistic dependency structure. utils.js is imported by multiple modules β that's perfectly valid and common. Each module only sees what it imports; auth.js cannot accidentally break api.js because they don't share a global scope.
4. Default vs Named Exports
This is one of the most common points of confusion. Here's the practical difference:
// βββ Exporting βββββββββββββββββββββββββββββββββββ
// Named β export multiple things
export const API_URL = 'https://api.example.com';
export const TIMEOUT = 5000;
// Default β export the main thing
export default class UserService { /* ... */ }
// βββ Importing βββββββββββββββββββββββββββββββββββ
// Named β must use curly braces and exact names
import { API_URL, TIMEOUT } from './config.js';
// Default β no curly braces, any name works
import UserService from './user-service.js';
import Users from './user-service.js'; // Also valid
// Both in one line
import UserService, { API_URL } from './user-service.js';
Recommendation: Prefer named exports for most cases. They're more explicit, easier to refactor (your editor knows the exact name), and enable better dead-code elimination.
5. Benefits of Modular Code
Adopting modules isn't just a syntax upgrade β it changes how you think about building software. Here's what you gain:
Quick Reference
| Syntax | What it does |
|---|---|
export function foo() {} |
Named function export |
export const X = 42 |
Named constant export |
export { a, b, c } |
Group multiple named exports |
export default foo |
Default export (one per file) |
import { a, b } from './f.js' |
Import named exports |
import foo from './f.js' |
Import default export |
import { a as myA } from './f.js' |
Import with rename |
import * as ns from './f.js' |
Import everything as namespace |
import foo, { a, b } from './f.js' |
Import default + named together |
<script type="module"> |
Enable modules in the browser |
happy coding...



