Barrel Export
Problem
Consider this import: import { Button } from './components/atoms/buttons/Button/Button.tsx'. You’ve exposed your entire folder structure in every file that uses the component, so moving one folder means updating fifty import statements.
I’ve seen codebases where refactoring was impossible because file paths were hardcoded everywhere, and developers had memorized the directory structure like they were studying for an exam. Deep relative paths like ../../../../components/Button are unreadable and break the moment you move anything.
Without clear module boundaries, consumers import internal implementation details you never intended to expose. That helper function you meant to keep private is now used elsewhere, and you can’t change it without breaking their imports.
Solution
Create index files at the root of each module directory that re-export your public API. Instead of importing from deep paths, consumers import from the directory itself with clean imports like import { Button } from './components', decoupled from your internal file organization.
This lets you reorganize freely: rename files, split them, or move them into subdirectories without breaking anything outside. Use barrel files as deliberate API boundaries, exporting only what external code should use while keeping internal helpers hidden.
Example
Here’s the pattern in action, from flat barrels to nested hierarchies.
// components/Button.js
export function Button(props) { /* ... */ }
// components/Input.js
export function Input(props) { /* ... */ }
// components/index.js - Barrel file
export { Button } from './Button';
export { Input } from './Input';
// Usage
import { Button, Input } from './components';
Nested Barrel Pattern
Barrels can nest: higher-level barrels re-export from lower-level ones to create organized API layers.
// components/forms/index.js
export { Input } from './Input';
export { TextArea } from './TextArea';
// components/buttons/index.js
export { PrimaryButton } from './PrimaryButton';
// components/index.js - Top-level barrel
export * from './forms';
export * from './buttons';
// Usage - Import from any level
import { Input } from './components/forms';
import { Input, PrimaryButton } from './components';
Namespace Imports
Barrel exports can be used as namespaces, grouping related exports under a single identifier.
import * as Components from './components';
const button = <Components.Button label="Click me" />;
const { Button, Card } = Components; // Can destructure
Selective Re-exporting
Rather than re-exporting everything with export *, selectively re-export specific items for better tree-shaking and explicit API control.
// utils/dateUtils.js
export function parseDate(str) { /* ... */ }
export function _internalHelper(date) { /* ... */ } // Internal
// utils/index.js - Selective exports (prefer over export *)
export { parseDate, formatDate } from './dateUtils';
export { capitalize, truncate } from './stringUtils';
// Internal helpers stay hidden
TypeScript Type Re-exports
Barrel files can re-export TypeScript types alongside values.
// components/index.ts
export { Button, type ButtonProps } from './Button';
export { Input, type InputProps } from './Input';
// Usage
import { Button, type ButtonProps } from './components';
Benefits
- Cleaner imports:
import { Button } from './components'beats a four-level deep path every time. - Easier refactoring: Move, rename, or split files freely without affecting external code as long as barrel exports stay stable.
- Explicit public APIs: It’s clear what external code should use versus internal implementation details.
- Hierarchical organization: Nested barrels let different parts of a large codebase expose their own focused APIs with clear boundaries at each level.
Tradeoffs
- Tree-shaking issues: Using
export *can prevent bundlers from eliminating unused code; prefer explicit named exports for better optimization. - Maintenance overhead: Every barrel file needs updating when you add or remove exports, and in large codebases they can get unwieldy. Consider splitting into focused sub-barrels.
- Harder source tracing: When you see an import, you have to check the barrel file first to find the actual source file.
- Circular dependency risk: Barrels that import from each other create nasty bugs that may not manifest until runtime.
Summary
Barrel exports consolidate multiple modules into a single entry point, so you import from one index file instead of memorizing exact paths. Use them judiciously for public interfaces while being mindful of tree-shaking and circular dependencies.