Skip to content

Index File Re-exports

Barrel files (index.js) re-export modules from a directory through a single entry point. Consumers import any symbol from one path instead of reaching into internal file structure.

When to use

Create an index file when a directory contains multiple modules that consumers import together — components, utilities, hooks, services — and you want a single import path that hides the internal file organization. The barrel file insulates importers from file renames and restructuring, and converts default exports to named exports at the public boundary.

The pattern

Leaf-level barrel files

Each subdirectory gets an index.js that re-exports its modules as named exports:

js
// components/forms/index.js
export { default as Button } from "./Button.vue";
export { default as DateInput } from "./DateInput.vue";
export { default as Input } from "./Input.vue";
export { default as Select } from "./Select.vue";

The export { default as Name } syntax converts a module's default export into a named export. This gives consumers a consistent named-import experience regardless of how each module declares its export internally.

Root aggregator

A parent index file re-exports everything from each subdirectory barrel:

js
// components/index.js
export * from "./common/";
export * from "./forms/";
export * from "./layout/";
export * from "./motion/";
export * from "./overlays/";

export * from forwards all named exports from the target module. Because each subdirectory barrel already converts defaults to named exports, the root aggregator gets clean named exports to forward.

Consumer imports

Consumers import from the root barrel without knowing the internal directory structure:

js
import { Button, Layout, Dialog } from "!/components/";
import { Header, Footer } from "!/components/";

Components from different subdirectories appear in a single import statement.

Two-tier structure

The pattern works in two layers:

components/
├── index.js              ← root aggregator
├── common/
│   ├── index.js          ← leaf barrel
│   ├── IconLink.vue
│   └── MaterialIcon.vue
├── forms/
│   ├── index.js          ← leaf barrel
│   ├── Button.vue
│   ├── Input.vue
│   └── Select.vue
└── layout/
    ├── index.js          ← leaf barrel
    ├── Header.vue
    └── Footer.vue
  1. Leaf barrels convert default exports to named exports
  2. Root aggregator merges all leaf barrels into one path

Adding a new component means adding one file and one line to the nearest leaf barrel. No other files change.

Framework-agnostic application

Although the examples above use Vue single-file components, the pattern applies to any module system with default or named exports:

js
// utils/index.js — plain JavaScript utilities
export { default as debounce } from "./debounce.js";
export { default as throttle } from "./throttle.js";
export { default as clamp } from "./clamp.js";
js
// composables/index.js — Vue composables
export { default as useAuth } from "./useAuth.js";
export { default as useForm } from "./useForm.js";
js
// services/index.js — service modules
export { default as ApiClient } from "./ApiClient.js";
export { default as Logger } from "./Logger.js";

The key constraint is that each module has a default export to convert. If modules already use named exports, re-export them directly:

js
export { formatDate, parseDate } from "./dates.js";

Trade-offs

Benefits:

  • Single import path for an entire module category
  • Internal file moves do not break consumer imports
  • Consistent named-export API at the public boundary
  • Easy to discover available exports by reading one file

Costs:

  • Every new module needs a barrel entry (one extra line)
  • Barrel files can cause larger bundles if the bundler does not tree-shake unused re-exports — modern bundlers (Vite, Rollup, webpack 5) handle this well, but verify with your build tool
  • Deep re-export chains (three or more tiers) can obscure where a symbol is defined — limit to two tiers in most cases
  • Name collisions across subdirectories surface at the root barrel; resolve by renaming at the leaf level with export { default as PrefixedName }