Free lesson · JavaScript Basic to Advance
Re-exports and Module Composition
Building Modules From Other Modules
Real applications aren't built from individual modules in isolation. They're built from groups of related modules that get composed together. The mechanism for this composition is re-exports — a module that exposes values defined in other modules as if they were its own.
// math/add.js
export function add(a, b) { return a + b; }
// math/multiply.js
export function multiply(a, b) { return a * b; }
// math/index.js — re-exports
export { add } from "./add.js";
export { multiply } from "./multiply.js";
// app.js — imports from the re-exporting "barrel"
import { add, multiply } from "./math/index.js";
// Or simply:
import { add, multiply } from "./math"; // if your runtime resolves index.js
The math/index.js file doesn't define add or multiply itself. It re-exports them from elsewhere. Consumers of the math directory don't need to know whether add lives in add.js or somewhere else — they import from the top-level entry point.
This Topic covers re-exports and the "barrel file" pattern — how libraries and large applications structure their public API.
Forms of Re-export
Several syntactic forms, all of which take a value from one module and re-emit it from the current one.
Named re-export — pass along specific named exports:
export { add } from "./add.js";
export { multiply, divide } from "./arithmetic.js";
This is equivalent to importing those names and then exporting them, but in one statement. The current module never has local variables for these values — they pass through directly.
Renamed re-export — re-export under a different name:
export { add as plus, multiply as times } from "./math.js";
The current module exposes plus and times. Importers see those names, not add and multiply.
Namespace re-export — re-export everything from another module:
export * from "./math.js";
This re-exports every named export from math.js. Useful when you want to forward everything from a sub-module to a parent. Note: it does NOT re-export the default export — you have to handle that separately.
Default re-exports — special syntax for default:
// Re-export a default as named
export { default as Math } from "./math.js";
// Re-export a default as default
export { default } from "./math.js";
// Re-export the default plus all named exports
export { default } from "./math.js";
export * from "./math.js";
The default export needs explicit handling because the basic export * syntax skips it.
The Barrel File Pattern
A barrel file is a module whose only job is re-exporting from other modules. By convention, it's usually called index.js so importers can refer to the directory name and get the barrel file automatically.
math/
├── add.js
├── multiply.js
├── divide.js
└── index.js ← barrel file
// math/index.js
export { add } from "./add.js";
export { multiply } from "./multiply.js";
export { divide } from "./divide.js";
Now any file in the project can import { add } from "./math" instead of import { add } from "./math/add.js". The directory becomes a single conceptual module from the consumer's perspective.
This pattern has real benefits:
1. Stable public API. Internal file structure can change. As long as the barrel file's exports stay the same, consumers aren't affected.
2. Single import point. Consumers write one import statement to get everything they need from a package or directory.
3. Easier to track usage. All exports from a feature module pass through one file. Code review for "what does this module expose?" becomes a single-file question.
graph TD
A[Consumer: app.js] -->|imports from| B[math/index.js<br/>barrel file]
B -->|re-exports| C[math/add.js]
B -->|re-exports| D[math/multiply.js]
B -->|re-exports| E[math/divide.js]
style B fill:#7c3aed,color:#fff
style A fill:#10b981,color:#fff
This diagram shows the barrel file pattern. A consumer module imports from a single entry point — typically named index.js inside a directory. The barrel file doesn't define values itself; it re-exports from multiple source files within the directory. The consumer sees a single module interface. Internally, the directory can reorganize its files without affecting any consumer.
The Tree-Shaking Trade-Off
There's an important downside to barrel files in some setups: they can hurt tree-shaking, the bundler optimization that removes unused code.
When you write import { add } from "./math", a smart bundler should be able to figure out that only add is used and exclude multiply and divide from the final bundle. In practice, this analysis depends on the bundler being able to trace through the barrel file correctly.
For modern bundlers (esbuild, Vite, well-configured Webpack), barrel files of pure re-exports work fine for tree-shaking. For older or misconfigured bundlers, barrel files can bring in everything from the barrel even if you only import one thing.
For library authors, this matters a lot — barrel files in a published npm package can balloon consumer bundle sizes. For application code, the trade-off is usually fine because final bundlers handle it.
If you encounter bundle-size issues from barrel files, the fix is usually to import directly: import { add } from "./math/add.js" instead of going through the barrel. Cleaner imports for cleaner builds.
Interactive animation: What a barrel buys against what it costs - one import path versus whole-folder loading and defeated tree-shaking.
Dynamic Imports
There's one form of import we haven't covered yet: dynamic imports with import() as a function. This isn't a re-export per se, but it's worth knowing about in this Topic because it's part of how modules compose.
async function loadFeature() {
const featureModule = await import("./feature.js");
featureModule.run();
}
// Or with destructuring
async function loadFeature() {
const { run } = await import("./feature.js");
run();
}
import() returns a Promise that resolves to the module's exports as an object. The module is loaded lazily — only when this function is called, not at startup.
This is useful for:
Code splitting. Load a feature only when the user accesses it.
Conditional loading. Load different modules based on environment, user input, or feature flags.
Performance optimization. Defer loading of large modules until they're actually needed.
The trade-off: dynamic imports are async, so consumers have to handle the Promise. Static imports give you values directly; dynamic imports give you Promises.
Interactive animation: Load the heavy thing only when clicked - import() inside a click handler loads the module lazily, not at startup.
When NOT to Over-Compose
A trap: applying the barrel pattern at every level produces too much indirection.
src/
├── features/
│ ├── auth/
│ │ ├── components/
│ │ │ ├── LoginForm/
│ │ │ │ ├── index.js ← barrel
│ │ │ │ └── LoginForm.jsx
│ │ │ └── index.js ← barrel
│ │ ├── index.js ← barrel
│ │ └── ...
│ └── index.js ← barrel
└── index.js ← barrel
Five levels of barrels for one component. Imports become deeply nested. Refactoring requires updating multiple barrel files. The "stable public API" benefit is gone because the public API itself is now a maze of forwarders.
A reasonable rule: one barrel per logical module or feature, not per directory. If your auth feature has many internal files, one auth/index.js barrel can be the entry point. But individual component directories don't usually need their own barrels.
Common Pitfalls
Interactive animation: Two modules importing each other - the cycle, and who survives it when the code runs.
Warning: The re-export and barrel traps.
Forgetting that
export *doesn't include defaults. If your source module has a default export, you need a separateexport { default } from "..."line to re-export it.Circular re-exports. A barrel that re-exports from a file that imports from the barrel creates a circular dependency. JavaScript handles these in subtle ways that can produce undefined values at runtime.
Bundle-size surprises. In poorly-configured projects, barrel files can defeat tree-shaking. If your bundle is larger than expected, check whether you can import directly from source files instead of through barrels.
Over-barreling. A barrel at every directory level adds indirection without benefit. Use barrels where they serve as deliberate public APIs, not as habit.
Why This Matters in Interviews
Interview: Barrel files and re-exports are intermediate-level interview territory. The question form is usually "how do you organize a large JavaScript codebase?" — and a senior answer mentions feature folders, barrel files for public APIs, and being deliberate about what's exported vs internal.
Topic 11.7 — Interview Lens: Module System Questions — covers the deeper treatment.
Try It
Exercise: Take three small modules —
add.js,subtract.js,multiply.js— each exporting one function. Build a barrel fileindex.jsthat re-exports them all. Then:
- Import from the barrel and verify it works
- Add a renamed re-export: expose
multiplyastimes- Add a new sub-directory
advanced/with one more module, and re-export from there through your barrel- Try
export *and see what happens with default exports if you add oneThe exercise solidifies the mechanical details. After this, the barrel pattern is something you can apply in real codebases without confusion.