Barrel Export
Aggregate multiple exports into a single entry point using index files to create clean public APIs and simplify import paths.
In this article
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 painful because file paths were hardcoded everywhere, and developers had to memorize the directory structure to write an import. Deep relative paths like ../../../../components/Button are hard to read 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
Each module directory gets an index file at its root that re-exports its public API. Consumers import from the directory itself, like import { Button } from './components', instead of from deep paths, so their imports don’t depend on how you organize files inside it.
This lets you reorganize freely: rename files, split them, or move them into subdirectories without breaking anything outside. Barrel files work best as deliberate API boundaries, exporting only what external code should use while keeping internal helpers hidden.
Example
The simplest barrel re-exports each component in a folder from one index file.
// 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 either level
import { TextArea } 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, Input } = Components; // Can destructure
Selective Re-exporting
Re-exporting specific items, rather than everything with export *, gives you explicit control over the API, so a new export in a source file doesn’t become public by accident.
// utils/dateUtils.js
export function parseDate(str) { /* ... */ }
export function formatDate(date) { /* ... */ }
export function _internalHelper(date) { /* ... */ } // Internal
// utils/stringUtils.js
export function capitalize(str) { /* ... */ }
export function truncate(str, length) { /* ... */ }
// 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'is easier to read than a four-level deep path. - 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
- Loading more than you use: Production bundlers like Rollup, esbuild, and webpack usually tree-shake barrels well, as long as the re-exported modules have no side effects. The bigger cost shows up in development. A dev server that serves modules unbundled, or a test runner like Jest, has to load every module a barrel re-exports, so importing one button from
./componentsloads every component in the folder, which slows down dev page loads and test startup in a large codebase. Next.js addedoptimizePackageImportsto work around this for large library barrels. - 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 can cause nasty bugs that only show up at runtime, such as a value that is
undefinedbecause its module hasn’t finished loading yet.
Summary
Barrel exports consolidate multiple modules into a single entry point, so you import from one index file instead of memorizing exact paths. They work best for public interfaces, with some care around how much code each barrel pulls in and around circular dependencies.