Modules
A JavaScript module is a self-contained file that exposes its code to other files using export and pulls in code from elsewhere using import , keeping each part of an app organized and reusable.
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
Part of the free JavaScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
💡 Running Code Locally: While this online editor runs real JavaScript, some advanced examples (like fetch to external APIs) may have limitations. For the best experience:
Think of JavaScript modules like LEGO bricks . Each brick (module) is self-contained with a specific shape and purpose. You can combine different bricks to build complex structures (applications), and if one brick breaks, you just replace that one instead of rebuilding everything. Just like LEGO sets come in organized boxes with labeled compartments, modules keep your code organized and manageable.
JavaScript modules completely changed how modern applications are built, transforming a language that once relied on global variables into a structured, scalable ecosystem capable of powering complex frontends, backend servers, mobile apps, and enterprise systems. ES6 modules introduced a standardised way to split code into files, share functions or classes, and control visibility.
Instead of dumping everything into a single script tag, modules allow you to create isolated, reusable units that behave predictably without polluting the global scope.
🔥 Basic Exports and Imports
At the core of the module system are exports and imports . Exports decide what parts of a file are accessible to other files. Imports allow you to pull those exported values into the environment where you need them.
The simplest example is a utility file exporting a function:
This may look basic, but behind the scenes it activates the module loader, which performs dependency resolution, module graph construction, caching, and execution ordering before any code runs.
🔥 Named Exports vs Default Exports
Different export styles give you different architectural patterns. A file can have multiple named exports , which is best when offering multiple utilities:
Named imports use curly braces because they map exactly to identifiers. Default imports do not because they represent a single primary value from that file. Named imports must match export names unless aliased:
🔥 Live Bindings — A Critical Concept
One key property is that imports are live bindings , meaning the value updates across modules. If one module modifies an exported variable (not reassigned, but mutated), all modules using it see the change.
This is the foundation of reactive libraries like Svelte, SolidJS, and Vue's Composition API, which rely on live module bindings to track changes.
🔥 How Browsers Load Modules
The browser downloads app.js , parses its import statements, and then recursively fetches all modules before executing anything. This behaviour ensures correct dependency ordering but also means modules load via separate network requests — which is why bundling exists.
Without bundling, an app with 200 imports would make 200 HTTP requests. While HTTP/2 and HTTP/3 minimise this overhead, bundlers still optimise dependency trees to reduce size.
🔥 Understanding Bundlers
Tools like Webpack, Rollup, Vite, esbuild, and Parcel take your ES6 modules, resolve import paths, tree-shake unused exports, minify code, inline small assets, rewrite URL references, and output a single (or few) build files.
A very simple bundler configuration for Rollup looks like:
Rollup is tree-shaking-first, making it excellent for libraries. Webpack is more configurable and supports loaders for CSS, images, fonts, and advanced optimisation.
Bundling also solves the difference between "bare imports" and relative paths. Native ES modules require relative paths like ./utils.js unless running in Node or using an import map. Bundlers allow simplifying paths:
🔥 Module Scope
Every module has its own top-level scope, so variables declared at the top of a module are not available globally. This solves many of the old problems seen in large applications where different files accidentally overwrote identifiers.
🔥 Dynamic Imports & Code Splitting
You can dynamically import modules based on user action:
This enables code-splitting — loading only the necessary code when needed, which massively improves performance. This technique is used everywhere: YouTube loads comments only when scrolling, Amazon loads product recommendations dynamically, and Meta loads notification panels as separate modules.
🔥 Tree-Shaking: When It Works and When It Fails
This is why all major libraries (React, Lodash-es, RxJS, date-fns) use pure named exports.
🔥 Module Caching
When a module is first imported, the engine creates a Module Record — containing its exports, its execution state, and pointers to dependent modules. After execution, this record is stored in cache so other imports return the same objects.
This means modules are effectively singletons. It's ideal for configuration, shared state, or global utilities, but dangerous for objects that must be reinitialised per user session.
🔥 Circular Dependencies — The Hidden Problem
Circular dependencies create hidden runtime errors:
Professional architecture avoids cycles using a layered dependency model:
🔥 Common Path Mistakes
❌ Mistake 1: Forgetting file extensions in browser ESM
❌ Mistake 2: Using absolute paths without import maps
❌ Mistake 4: Dynamic import with incorrect path resolution
🔥 The "Barrel File" Pattern
A barrel file (index.js) re-exports everything inside a folder:
Barrels create a structured module ecosystem that tools interpret as organised, high-value content.
🔥 Namespace Imports
Namespace imports create a frozen module object:
This prevents accidental corruption of shared modules and enables optimisations inside bundlers and JIT engines.
🔥 Professional Module Patterns
1. "Pure Module" Pattern
No top-level side effects. Everything is callable. Best for library design.
2. "Service Module" Pattern
3. "Facade Pattern" for Large Apps
Modules act as simplified interfaces hiding complexity.
🔥 Module Pattern Architecture For Scalable Projects
This creates a clean, predictable import system:
🔥 Security Considerations
❌ Never dynamically import unvalidated user-provided paths
🔥 Bundling Pitfalls Beginners Always Hit
🎯 Key Takeaways
Understanding ES6 modules isn't just about syntax — it's about understanding how your entire application architecture behaves under real production conditions. Every decision you make about import structure, export styles, bundling configuration, module boundaries, and dynamic loading impacts performance, caching behaviour, security, and scalability.
Practice quiz
What does a JavaScript module use to expose its code to other files?
- require
- global
- export
- window
Answer: export. A module exposes code with export and pulls in code from elsewhere with import.
When should you prefer a default export?
- When a file has one main thing to export
- When offering many utilities
- Never
- Only for constants
Answer: When a file has one main thing to export. Default export is for the one main thing per file; named exports suit multiple utilities.
Why do named imports use curly braces while default imports do not?
- It is just style
- Braces make imports faster
- Default imports are deprecated
- Named imports map exactly to identifiers; a default is a single primary value
Answer: Named imports map exactly to identifiers; a default is a single primary value. Named imports map to specific exported identifiers, so they need braces; defaults are one value.
What does it mean that imports are 'live bindings'?
- They are copies frozen at import time
- Changes to an exported value are seen across all importing modules
- They reload on every access
- They only work in Node
Answer: Changes to an exported value are seen across all importing modules. Imports are live bindings, so a mutation to an exported value is visible to all modules using it.
What does tree-shaking do?
- Removes unused exports from the bundle
- Reorders imports alphabetically
- Minifies variable names
- Caches network requests
Answer: Removes unused exports from the bundle. Tree-shaking removes unused exports; if you only import add, the bundler drops multiply.
Why can a default-exported object block tree-shaking?
- It is always larger
- Default exports aren't bundled
- The entire object must remain since individual members can't be removed
- It causes circular dependencies
Answer: The entire object must remain since individual members can't be removed. Exporting an object as default means nothing inside it can be tree-shaken; the whole object stays.
What do dynamic imports with await import() enable?
- Synchronous loading
- Code-splitting — loading modules on demand
- Global variables
- Removing all imports
Answer: Code-splitting — loading modules on demand. Dynamic imports load modules only when needed, enabling code-splitting for better performance.
Because modules are cached after first import, they effectively behave as what?
- New instances each time
- Global variables
- Copies
- Singletons
Answer: Singletons. The Module Record is cached, so every import returns the same objects — modules are singletons.
What does each module have that prevents global namespace pollution?
- A shared global scope
- Its own top-level module scope
- Automatic var hoisting
- A required IIFE
Answer: Its own top-level module scope. Every module has its own top-level scope, so top-level variables aren't global.
How does professional architecture avoid circular dependencies?
- By using only default exports
- By disabling caching
- With a layered dependency model that never imports upwards
- By using require instead of import
Answer: With a layered dependency model that never imports upwards. A layered model like components → services → utils → constants, never importing upward, avoids cycles.