Split your code into neat, reusable files that work together like LEGO sets
Why Modules?
Imagine building a house where every single piece of furniture, wiring, and plumbing is crammed into one giant room. Chaos, right? Modules let you split your code into separate files, each with a clear purpose. One file handles math utilities, another handles user authentication, another handles the UI.
Each module has its own scope — variables inside a module don't leak into other modules unless you explicitly export them. Other files can then import exactly what they need. This keeps your code organized, avoids naming conflicts, and makes it easy to reuse code across projects.
JavaScript modules use import and export statements. In browsers, you need <script type="module">. In Node.js, you can use .mjs files or set "type": "module" in package.json.
Named Exports & Imports
// ─── math-utils.js ───
exportconstPI=3.14159;
exportfunctionadd(a, b){
return a + b;
}
exportfunctionmultiply(a, b){
return a * b;
}
functionsecretHelper(){
// not exported — stays private to this file!
return42;
}
// ─── main.js ───
import{ add, multiply,PI}from"./math-utils.js";
console.log(add(2,3));// 5
console.log(multiply(4,5));// 20
console.log(PI);// 3.14159
// Rename on import to avoid conflicts
import{ add as sum }from"./math-utils.js";
console.log(sum(10,20));// 30
// Import everything as a namespace
import*asMathUtilsfrom"./math-utils.js";
console.log(MathUtils.add(1,2));// 3
console.log(MathUtils.PI);// 3.14159
Output
5
20
3.14159
30
3
3.14159
Default Exports
Each module can have one default export. It's the "main thing" the module provides. When you import a default export, you can name it whatever you want — you don't need curly braces.
Think of named exports like items in a store (you pick specific ones by name), and the default export like the store's signature product (there's only one, and everyone just calls it "the thing from that store").
A common convention: use default exports for a module's primary class or component, and named exports for utilities and constants.
Dynamic imports use import() as a function (not a statement). They return a Promise and load the module on demand — perfect for code-splitting, where you only load code when the user actually needs it. This makes your app faster because you're not loading everything upfront.
Re-exports let you create an "index" file that gathers exports from multiple modules and re-exports them from one place. This is a clean pattern for organizing libraries.
Dynamic Imports & Barrel Files
// Dynamic import — loads a module at runtime (returns a Promise)
Note: When you import { something } from a module, you're not copying the value — you're creating a live link to it. If the exporting module changes the value later, your import sees the update. This is different from CommonJS (require), where you get a snapshot. Most of the time this doesn't matter, but it's good to know!
Quick check
What's the difference between named and default exports?