Skip to content
Intermediate10 min study

JavaScript modules

Organize code with static imports, live bindings, encapsulation, and predictable loading boundaries.

Question progress0 / 10 completed
Start the lesson
JavaScript modules visual explanation

WHAT YOU WILL BE ABLE TO DO

Learning outcomes

  • Explain javascript modules in plain language.
  • Connect the behavior to the underlying browser or framework model.
  • Implement the core pattern and reason through edge cases.
  • Answer common follow-ups without relying on memorized phrases.

01Explain it simply

A module is a file with its own top-level scope and explicit imports and exports. ES modules let tools understand dependencies before running the code, which enables bundling, code splitting, and dead-code elimination.

Think of it like this: Think of a module like a shop with one counter: customers can only get what the shop deliberately puts on display — its exports — everything in the back room stays private and unreachable, and the shop's inventory is set up exactly once no matter how many customers walk in afterward.

One-line definition: Organize code with static imports, live bindings, encapsulation, and predictable loading boundaries.

02Mental model

Imports are live read-only views of exported bindings, not copied values. A module is evaluated once per realm and then cached, so module-level state behaves like a singleton within that environment.

03Step by step

  • Export a narrow public API.
  • Keep implementation details unexported.
  • Use named exports for discoverability.
  • Use dynamic import only at genuine loading boundaries.
  • Avoid circular dependencies by moving shared contracts downward.

04Working example

JavaScript
// cart.jslet items = [];export const addItem = (item) => { items = [...items, item]; };export const getTotal = () => items.reduce((sum, item) => sum + item.price, 0);
// checkout.jsimport { getTotal } from './cart.js';

The items binding remains private. Consumers can use the exported behavior without mutating the module's internal collection directly.

05Where it is used

  • Feature boundaries
  • Shared utilities and design systems
  • Route-level code splitting
  • Separating browser-only and server-only code

06Common mistakes

  • Large barrel files that create accidental coupling
  • Circular imports whose initialization order becomes fragile
  • Default exports renamed inconsistently across a codebase
  • Putting request-specific server state at module scope

07Interview answer

How to say it out loud: "An ES module is a file with its own scope — nothing inside it is global unless it's explicitly exported. When another file imports a value, it gets a live, read-only reference to that binding, not a copied snapshot, so if the exporting module updates the value later, the importer sees the new value too, even though it can't reassign it directly. Modules are also evaluated exactly once and then cached, which is why module-level state behaves like a singleton within that environment, and it's also what lets bundlers statically analyze imports and exports to do tree-shaking and code-splitting before any code actually runs."

Mention static analysis, live bindings, one-time evaluation, and dynamic import as a deliberate performance boundary.

Does an ES module import copy the exported value?

No. It observes the export's live binding, although the importing module cannot reassign that binding.

DDConcept deep dives

Deep dive 1

Static structure enables tooling

Import and export declarations are syntactically restricted and can be analyzed without executing the module. Browsers and build tools construct a dependency graph, link live bindings, and determine evaluation order. Bundlers use that structure for code splitting, tree shaking, and chunk planning, though side effects and interop can constrain optimization.

  • Imports are hoisted declarations but dependencies must still initialize correctly.
  • Named exports make API use statically visible.
  • Top-level module code is strict and scoped to the module.

Deep dive 2

Modules are evaluated once per graph instance

After linking, each module is evaluated once and later importers reuse its exports. Mutable module-level state therefore behaves like a singleton within that realm or server module cache. That can be deliberate for shared browser services, but request-specific server data at module scope can leak across users.

  • A live binding can change after import; it is not a copied snapshot.
  • Consumers cannot assign to an imported binding.
  • Tests must reset or isolate intentional singleton state.

Deep dive 3

Loading boundaries need product intent

Dynamic import delays fetching and evaluation until execution reaches the boundary. Good boundaries surround optional tools, routes, or expensive modes the user may never open. Splitting every small module increases requests and runtime bookkeeping, while barrel exports can accidentally pull lazy code back into an eager graph.

  • Measure bundle composition in a production build.
  • Preload likely next interactions without forcing all code into the initial path.
  • Cycles make initialization fragile; move shared contracts downward or invert ownership.

QAInterview questions and model answers

Attempt each answer aloud before opening it. The model answer shows the depth and precision expected in an interview; it is not a script to memorize.

Intermediate · Conceptual · 1 min · Question 1What distinguishes an ES module from a classic script?Open model answer

Model answer

An ES module has its own top-level scope, uses static import/export syntax, runs in strict mode, and is evaluated once per module graph instance. Browser modules are deferred by default and participate in dependency graph loading.

Open question page →
Intermediate · Coding · 1 min · Question 2Are imported values copied?Open model answer

Model answer

No. Imports are read-only views of live exported bindings. If the exporting module updates a binding, importers observe the new value. Importers cannot assign to the imported binding.

JavaScript
// counter.jsexport let count = 0;export const inc = () => { count++; };
// main.jsimport { count, inc } from './counter.js';inc();console.log(count); // 1 — live view, not a snapshotcount = 5;          // TypeError — imports are read-only
Open question page →
Intermediate · Coding · 1 min · Question 3How does dynamic import help performance?Open model answer

Model answer

import() returns a promise and creates a runtime loading boundary that bundlers can turn into a separate chunk. Use it around optional or route-level functionality; excessive tiny chunks add network and execution overhead.

JavaScript
button.addEventListener('click', async () => {  const { openEditor } = await import('./editor.js'); // separate chunk  openEditor();});
Open question page →
Intermediate · Conceptual · 1 min · Question 4What makes circular dependencies dangerous?Open model answer

Model answer

ES modules can represent cycles, but code may access a binding before the exporting module has initialized it. Cycles also signal tangled ownership and make evaluation order harder to reason about. Move shared contracts to a lower-level module or invert the dependency.

Open question page →
Intermediate · Conceptual · 1 min · Question 5What is tree shaking?Open model answer

Model answer

It is build-time removal of exports proven unused, enabled by the static shape of ES module imports and exports. Side effects, dynamic access patterns, CommonJS interop, and inaccurate package metadata can limit safe elimination.

Open question page →
Intermediate · Conceptual · 1 min · Question 6Why can module-level state be unsafe on a server?Open model answer

Model answer

A server module can be cached across requests, so mutable top-level state may leak or mix request-specific data. Keep request state within the request lifecycle and use deliberate external stores for shared state.

Open question page →
Intermediate · Conceptual · 1 min · Question 7What is a namespace import?Open model answer

Model answer

import * as ns creates a module namespace object exposing the module's exported bindings as read-only properties with live values.

Open question page →
Advanced · Conceptual · 1 min · Question 8How do default and named exports differ architecturally?Open model answer

Model answer

A default export offers one distinguished value and can be renamed freely by importers. Named exports make API names consistent and support explicit static discovery.

Open question page →
Advanced · Conceptual · 1 min · Question 9What does sideEffects in package metadata communicate?Open model answer

Model answer

It helps bundlers decide which files may be removed when exports are unused. Incorrectly marking side-effectful modules as pure can delete required initialization.

Open question page →
Advanced · Conceptual · 1 min · Question 10How are browser module URLs resolved?Open model answer

Model answer

Browsers resolve relative or absolute URL-like specifiers directly; bare specifiers require an import map or tooling. Each resolved URL identifies a module resource.

Open question page →

SCScenario questions

Scenario 1

A route imports a large editor dynamically, but the initial bundle still includes it. What would you inspect?

  1. Inspect the bundle graph rather than assuming the import created isolation.
  2. Look for static imports or barrel re-exports reaching the editor.
  3. Check shared side-effectful modules and bundler boundaries.
  4. Measure both transferred and executed JavaScript after the fix.
Reveal worked answer

A second static path can pull the editor into the initial graph, and a barrel file may hide that path. I would use the framework's bundle analyzer, import the editor directly only inside the dynamic boundary, remove accidental eager imports, and verify the network waterfall and main-thread execution on a clean production build.

Verify and go deeper