JavaScript Modules: Import and Export Explained
The problem: one file, all of everything When you start learning JavaScript, one file feels fine. A hundred lines of code, a few functions, a couple of event listeners — it all fits. Then the project grows.
Suddenly you have utility functions, UI logic, API calls, and global state all tangled together in a single script.js that's grown to 1,200 lines. You want to use a function from line 340 in a new feature, but it depends on three variables defined elsewhere, and one of them shares a name with something else you already have.
script.js — the "everything file" before modules // auth logic var currentUser = null; function login(email, password) { /* ... / } function logout() { / ... */ }
// product logic var products = []; function fetchProducts() { /* ... / } function renderProducts() { / ... */ }
// cart logic var cart = []; function addToCart(id) { /* ... / } function removeFromCart(id) { / ... */ }
// utility helpers function formatCurrency(n) { /* ... / } function debounce(fn, ms) { / ... */ }
// 1,200 more lines below...
This causes three specific problems:
Name collisions. Everything lives in the global scope. Any function or variable can accidentally overwrite another, including ones from third-party scripts. No clear ownership. When a bug appears, you have to search the entire file to find where a value is defined and where it's being changed. Can't reuse across projects. Your formatCurrency helper is useful in another project — but to take it, you have to manually copy it out from a massive file, along with everything it depends on. Modules solve all three. They let you split code into separate files, control exactly what each file exposes, and import only what you need.
02 — Exporting: what a module shares By default, everything inside a module is private. No other file can see it. To make something available to the outside world, you explicitly export it.
There are two kinds of exports: named exports and a default export. Let's look at named exports first.
Named exports You can export multiple values from a single file, each with its own name.
utils/math.js named exports // Export directly on declaration export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }
export const PI = 3.14159;
// Or export all at once at the bottom // export { add, multiply, PI };
Default export A module can also have one default export — typically the module's main thing, whether that's a class, a function, or an object.
utils/formatCurrency.js default export // Default export — one per file, no curly braces needed export default function formatCurrency(amount, currency = "USD") { return new Intl.NumberFormat("en-US", { style: "currency", currency, }).format(amount); }
03 — Importing: pulling in what you need import statements go at the top of the file. They tell JavaScript: before running this code, go fetch these values from those files.
Importing named exports main.js importing named exports import { add, multiply, PI } from "./utils/math.js";
console.log(add(2, 3)); // 5 console.log(multiply(4, PI)); // 12.56636
// You can rename on import with 'as' import { add as sum } from "./utils/math.js"; console.log(sum(10, 20)); // 30
Importing the default export main.js importing default export // No curly braces — and you can name it whatever you like import formatCurrency from "./utils/formatCurrency.js";
console.log(formatCurrency(1999.99)); // $1,999.99 console.log(formatCurrency(49.5, "GBP")); // £49.50
Importing both at once main.js mixing default + named // Default import comes first, then named in curly braces import formatCurrency, { add, PI } from "./utils/math.js";
Namespace import. If you want everything a module exports under a single object, use import * as Math from "./utils/math.js". Then access values as Math.add(), Math.PI, etc. Useful when importing many things, or when you want to be explicit about the source.
Diagram — Module import / export flow math.js export function add() {} export function multiply() {} export const PI = 3.14; formatCurrency.js export default function formatCurrency() {} api.js export function fetchUser() {} export function saveData() {} export const BASE_URL = "..." main.js import { add, multiply, PI } from "./utils/math.js"; import formatCurrency from "./utils/formatCurrency.js"; import { fetchUser, BASE_URL } from "./utils/api.js"; // ... use them here named default named 04 — Default vs named exports This is the question that trips up most beginners. The rule is simple once you see it visually, but the syntax difference — curly braces vs no curly braces — matters.
Feature Named export Default export How many per file? Many One only Export syntax export function foo() {} export default function() {} Import syntax import { foo } from "..." import foo from "..." Curly braces? Yes No Can rename on import? With as keyword Any name works natively Best used for Utility collections, constants A module's main class / function . The most common import mistake. Forgetting curly braces on a named import — or accidentally adding them to a default import — gives you undefined with no error. If an imported value behaves unexpectedly, check your braces first.
// Wrong — add() is named, needs braces import add from "./math.js"; // undefined
// Right import { add } from "./math.js";
// Wrong — formatCurrency is default, no braces import { formatCurrency } from "./formatCurrency.js"; // undefined
// Right import formatCurrency from "./formatCurrency.js"; 05 — Using modules in the browser To use ES modules directly in a browser — without a bundler like Webpack or Vite — you add type="module" to your script tag.
index.html html
Module scripts are automatically deferred. They don't block HTML parsing and always run after the page is ready — so you don't need to add defer manually. They also run in strict mode by default.
A realistic folder structure with modules might look like this:
// Project structure my-app/ index.html main.js ← entry point utils/ math.js ← named exports formatCurrency.js ← default export api.js ← named exports components/ Header.js ProductCard.js Diagram — File dependency graph index.html entry point loads main.js orchestrates import import import components/ Header.js components/ ProductCard.js utils/ api.js utils/math.js shared utility — used by multiple files shared dep. direct import Notice how math.js is imported by both Header.js and ProductCard.js. The browser is smart enough to only fetch and execute it once, no matter how many modules import it. This is one of the most important properties of the module system.
06 — Benefits of modular code, Private scope Variables and functions inside a module are invisible to the outside world unless explicitly exported. No more accidental global collisions.
Reusability A well-written utility module can be imported into any project without copying code. Write once, use everywhere.
Discoverability When everything has its own file with a clear name, finding where a function lives becomes trivially easy.
Testability Isolated modules with explicit inputs and outputs are much easier to unit test. Import the function, test it, done.
A real before/after Here's the same currency-formatting code — once as a tangled global function, once as a proper module:
Before — global function in script.js ,coupled
// Defined globally — anyone can overwrite it, nothing declares where it came from. function formatCurrency(amount) { return "$" + amount.toFixed(2); }
// Used somewhere else in the same giant file displayPrice(formatCurrency(cart.total));
After — utils/formatCurrency.js modular // utils/formatCurrency.js export default function formatCurrency(amount, currency = "USD") { return new Intl.NumberFormat("en-US", { style: "currency", currency, }).format(amount); }
cart.js consumer // cart.js — imports only what it needs import formatCurrency from "./utils/formatCurrency.js";
function renderCartTotal(total) { document.getElementById("total").textContent = formatCurrency(total); }
What about bundlers? Tools like Vite, Webpack, and Rollup build on top of the ES module system. They let you use import/export freely and then bundle everything into optimised files for production. The syntax you've learned here is exactly what those tools expect — understanding modules is the prerequisite to understanding bundlers.
What to take away from this article Without modules, all your code shares one global scope — leading to name collisions and unmaintainable files. Every module file has its own private scope. Exports are an explicit contract. Named exports use curly braces on import ({ add }); a file can have many. Default exports use no braces (import formatCurrency); a file can have one. Use type="module" in your script tag to enable native modules in the browser. A module imported by multiple files is fetched and executed exactly once — shared efficiently. Modular code is easier to read, test, and reuse across projects.
