Free lesson · JavaScript Basic to Advance
Interview Lens: Module System Questions
The Question
Module system questions cluster around four patterns:
"What's the difference between
requireandimport?""What's the difference between a default export and a named export?"
"How does Node resolve a bare import like
lodash?""Why is the difference between live bindings (ES Modules) and snapshots (CommonJS) important?"
These come up at mid-to-senior interviews. They test whether you understand module mechanics — not just the syntax, but the semantics and the ecosystem context.
I (Olena) covered the technical material in Topics 11.1 through 11.6. I'll handle the interview-prep treatment here. Arun joins for the interview-strategy framing — module questions sometimes appear early in interviews as warm-ups, and the candidate's depth of answer signals how the rest of the technical interview should be pitched.
Why They Ask It
Modules are foundational. A candidate who can't articulate the module system can't articulate how production JavaScript is organized. Almost every real project uses modules; almost every PR review involves module-related decisions; almost every bug investigation eventually touches imports and exports.
Three signals interviewers listen for:
Signal 1: Do you understand the runtime difference, not just the syntax? Many candidates can write both import and require syntax. Fewer can explain why the two systems behave differently — synchronous vs static, snapshots vs bindings. The deeper answer signals deeper understanding.
Signal 2: Have you actually shipped code that uses modules at scale? Questions about barrel files, tree-shaking, lockfiles, and version ranges separate "studied for interviews" from "worked on real projects." Specific stories about module-related debugging are valuable here.
Signal 3: Can you make the right design choices? "When would you use a default export?" "When would you use a barrel file?" "How would you structure modules in a large project?" These are judgment questions, and senior candidates have opinions backed by experience.
Arun here. In my fourteen rejections, module questions came up about a third of the time. They were rarely the make-or-break question, but they were almost always early in the interview — a calibration check. Getting the basics right confidently set the tone for the deeper questions that followed. Hesitating on import vs require made the interviewer think I'd be hesitant on harder things too.
Interactive animation: The package-surface checklist - direct imports inside, one barrel at the boundary, and named exports as the policy.
The Wrong Answers
Warning: The module-system mistakes interviewers see most often.
"
requireandimportare the same thing, just different syntax." They're not. They have fundamentally different semantics: synchronous vs static, snapshots vs bindings, dynamic vs analyzable. Treating them as equivalent signals shallow understanding."Default exports are always best because they're simpler." No. Modern style guides increasingly recommend named exports for refactorability and tooling reasons. The simplification is superficial.
"You should never have circular dependencies." Sometimes true, but reality is more nuanced — circular dependencies are sometimes unavoidable in graphs of related modules. ES Modules handle some circular cases that CommonJS doesn't. The senior answer addresses when circulars work and when they don't.
"npm just downloads packages." It does more — version resolution, lockfile generation, script execution, dependency tree management. A surface-level answer about npm signals limited experience.
"I just use
*for versions since I want the latest." No. Pinning to*is asking for production breakage. Sensible version ranges (^) are the modern default.
The Right Answer
Three layers depending on the depth being probed.
The 30-Second Answer
For "what's the difference between require and import?":
"require is CommonJS — Node.js's original module system. It's synchronous, dynamic, and exports values as snapshots. import is ES Modules — the language standard since 2015. It's static, can be asynchronous, and exports live bindings. Modern code defaults to import; legacy and many existing npm packages still use require."
For "default exports vs named exports":
"Default exports: at most one per module, the importer chooses the name. Named exports: any number per module, names must match (or be explicitly renamed). Modern style guides increasingly prefer named exports because they're easier to refactor and easier for tools to analyze."
The 2-Minute Answer
Build on the above:
"The mechanical difference between require and import is that import is a structural language feature — the runtime sees import statements and resolves them before any code runs. require is a regular function call that does file loading and module evaluation at the moment it's invoked.
This has real consequences. Because ES Modules are static, tools like bundlers and IDEs can analyze the entire dependency graph without running the code. That's what enables tree-shaking, type-checking across modules, and reliable auto-imports. With CommonJS, those tools have to do harder analysis because require calls could be anywhere.
The binding vs snapshot difference matters in practice. If you import a module that re-exports a value from elsewhere, and that elsewhere updates its value, the ES Module import sees the update. The CommonJS import doesn't. This is rarely something you exploit deliberately, but it explains some confusing behaviors.
For default vs named exports, the modern position is to prefer named exports because they're easier to refactor. The same value imported under different names across a codebase makes search and rename harder. Named exports enforce consistency. The exception: React components, where the convention is strong and one component per file is the norm."
The Deep Dive
For senior or staff interviews probing further:
"The technical reason ES Modules had to be static comes down to network loading in browsers. CommonJS was designed for Node.js, where the filesystem is fast and synchronous loads are fine. In a browser, you can't synchronously load a script — the page would freeze waiting for the network. ES Modules' static structure lets the browser fetch all dependencies in parallel based on the import graph, before any code runs.
The 'live binding' behavior of ES Modules has a specific implementation: imports are essentially aliases into the source module's variable environment. The importing module doesn't have its own copy of the value — it has a reference to the source module's slot. This is similar to how you can think of imports as pointers, except they're read-only.
The module resolution algorithm matters too. When you write import 'lodash', Node walks up the directory tree looking in node_modules at each level. This walking enables nested dependencies (different versions of the same library at different points in the tree) but also produces the disk-usage problem that pnpm solves with content-addressed storage.
The npm ecosystem's heavy transitive dependency problem comes from the 'small modules' culture combined with semantic versioning ranges. A single direct dependency can transitively pull in dozens because each of those small modules has its own dependencies. The lockfile mechanism is what makes this manageable at scale — without lockfiles, transitive dependency updates could change behavior unpredictably between developers' machines.
For circular dependencies: ES Modules handle them by exposing live bindings, so the imported value can be undefined at first and become defined later as both modules finish initializing. CommonJS handles them by exposing whatever's in module.exports at the moment of require, which can be incomplete. Either system can fail mysteriously if you depend on the wrong order of evaluation. The right fix is usually to break the cycle by restructuring."
This level of answer signals real depth. Use it when staff engineers are being evaluated.
Walking Through a Real Question
A common interview question: "I have two files, a.js and b.js. a.js imports from b.js, and b.js imports from a.js. What happens?"
Walk through it out loud:
"That's a circular dependency. With ES Modules, the runtime starts loading a.js. It hits the import statement for b.js. It starts loading b.js. b.js hits the import for a.js, but a.js is already being loaded — the runtime can't recursively load it. So b.js gets a partially-loaded view of a.js — whatever exports have been defined up to that point in a.js are available; whatever comes after the import of b.js isn't yet.
b.js finishes loading using that partial view. Then a.js resumes from where it left off and finishes.
If b.js only uses things from a.js after b.js has fully loaded (in callbacks, in functions called later), the live binding nature of ES Modules makes this work — by the time b.js's code actually accesses a.js's exports, they're fully defined.
If b.js uses a.js's exports at the top level during loading, they'll be undefined or partial, and you'll get bugs.
The same scenario in CommonJS produces snapshot-based partial loading, which is similarly fragile but in a slightly different way.
The practical fix: avoid circular dependencies where you can. Restructure shared code into a third file that both a.js and b.js import from."
This answer demonstrates understanding of both module systems' loading semantics. It's the kind of answer that scores well.
Interactive animation: The four projector rows - live binding against snapshot, then a circular import with an eager value and a late function call.
The Follow-Ups
Interview: What the interviewer asks next.
"How does tree-shaking work?" Static analysis of
importstatements identifies which exported values are actually used. Unused exports are eliminated from the final bundle. ES Modules' static nature is what enables this — CommonJS's dynamicrequiremakes it harder."What's a peer dependency?" A package declared in
peerDependenciesis a dependency the consumer is expected to provide. Common in plugin ecosystems — a React plugin declaresreactas a peer dependency to avoid duplicating React in the consumer's bundle."Why does my bundle have two copies of
react?" Usually because two transitive dependencies declared incompatible version ranges, so npm installed both. Lockfile inspection ornpm ls reactshows where each came from."What's the difference between
npm installandnpm ci?"installupdates the lockfile if needed.ciis strict — it fails ifpackage.jsonand lockfile are inconsistent, and it's faster for CI environments. Useciin production builds; useinstallduring development."When would you use dynamic
import()?" Code splitting, lazy loading, conditional features, polyfills. Anywhere you need module loading at runtime rather than upfront.
The Founder Moment
Olena here. The module-system question that taught me the most came up in my Microsoft interview loop. I'd been working with modules for years, but the interviewer asked a question I hadn't thought about: "Why does ES Module syntax require file extensions in browsers but not in bundler projects?"
I gave the surface answer — "bundlers resolve them automatically." The interviewer waited. I thought harder.
"The bundler can't resolve them at runtime in the browser, so it does it at build time. The browser sees the resolved path. Without a bundler, the browser has to load whatever path you give it, exactly. Adding extensions automatically would require runtime resolution, which the spec deliberately doesn't do."
The interviewer nodded. "Yes, that's right. Many candidates don't think about why this distinction exists."
What I took from that: the small inconsistencies in JavaScript usually have specific historical or technical reasons. The module system has many of these — the type field, the exports field, the extensions question, the CJS/ESM interop rules. Each one has a reason if you dig in.
The lesson for interviews: when an interviewer probes a small detail, they're usually testing whether you've thought about the system deeply or just memorized the surface. Take a moment, reason out loud, derive the answer from first principles. That signals senior engineering far more than reciting a fact.
Modules look mundane. They're not. They're where almost every real engineering decision in a JavaScript codebase gets made — what's public, what's private, what's coupled, what's not. Master them and a lot of other things become easier.