Now Reading:

What Are Barrel Files? Why They Matter And When Not To Use Them.

When writing code with typescript or your favourite javascript framework, you'll often organise your project into multiple files and folders. As your app grows, your import statements can quickly become unwieldy and repetitive.

A barrel file is a file that only exports other files and contains no code itself. Usually this will be a index.ts or index.js that lives in a directory and consolidates exports from multiple modules in that directory.

While this does offer some benefit, as your project grows, it starts to have an impact on the speed of your tooling.

Anatomy Of A Barrel File

#

Let’s start with an example of a typical directory structure.


_10
/src
_10
└──/utils
_10
├── math.ts
_10
├── date.ts
_10
├── validation.ts
_10
└── string.ts

This should look fairly familiar to you. We have a directory of files each containing their own concise amount of magic to get us out of a tricky coding situation. As we write code, we want to use these little magical nuggets of functional computation and can call on them in the following way.


_10
import { add } from "./utils/math";
_10
import { camelCase } from "./utils/string";
_10
import { formatDate } from "./utils/date";
_10
import { validateEmail } from "./utils/validation";

While this isn’t the worst thing in the world, doesn’t this look much nicer?


_10
import { add, camelCase, formatDate, validateEmail } from "./utils";

That’s where barrel files come into play.


_10
/src
_10
└──/utils
_10
├── math.ts
_10
├── date.ts
_10
├── validation.ts
_10
├── string.ts
_10
└── index.ts #<-- "Here be barrel file"

If we were to look in index.ts it would look something like this.


_10
// src/utils/index.ts
_10
export * from "./math";
_10
export * from "./date";
_10
export * from "./validation";
_10
export * from "./string";

It acts as a single entry point that entices us with the promise of making our module imports cleaner and easier to maintain, but as always there are trade offs with such benefits.

Stay ahead of the pack 🐶

Join the newsletter to get the latest articles and expert advice directly to your inbox.
Totally free, no spam and you can unsubscribe anytime you want!

  • Expert Tips
  • No Spam
  • Latest Updates

I'll never share any of your information with a third party. That's a promise.

A Barrels Effect On Bundle Size

#

As with most things that happen with writing code, it’s the scale which normally ends up catching us out.

The issue arises when using a barrel file that re-exports everything in a directory with export * or when importing unused code from the barrel file.

This is what we do without a barrel file.


_10
// utils/math.ts
_10
export const add = (a, b) => a + b;
_10
export const subtract = (a, b) => a - b
_10
_10
// App.tsx
_10
import { add } from "./utils/math";

Here, only the add function is imported from math.ts, and modern build tools like Webpack or Rollup can perform tree-shaking to remove any unused code during bundling, brilliant!

The same situation using a barrel file looks more like this.


_10
// utils/index.ts
_10
export * from "./math";
_10
export * from "./date";
_10
export * from "./validation";
_10
export * from "./string";
_10
_10
// App.tsx
_10
import { add } from "./utils";

When you import from a barrel file, your build tool might include everything re-exported by the barrel, even if you only use add. This can result in unused functions like subtract sneaking into your final bundle, but not only everything exported from math.ts but also everything from date.ts and validation.ts .

If the re-exported modules are large or depend on third-party libraries, things can quite quickly balloon out of control and significantly bloat your bundle size.

A Barrels Effect On Build Time

#

The primary reason barrel files might increase build times boils down to dependency resolution**.** When your favourite build tool like Webpack, Vite or Rollup processes a file, it needs to resolve every import statement. Barrel files introduce a layer of indirection because instead of importing directly from the source file, the build tool first has to process the barrel file, resolve its re-exports, and then find the actual source files.

The scenario is similar to what causes the bundle size to increase. Without using a barrel file we can simply import a module using the following syntax.


_10
// App.tsx
_10
import { add } from "./utils/math";

When the build tool processes this import, it directly resolves add from math.ts. The resolution is straightforward, with no intermediate steps. When we are doing this using a barrel file…


_10
// utils/index.ts
_10
export * from "./math";
_10
export * from "./date";
_10
export * from "./validation";
_10
export * from "./string";
_10
_10
// App.tsx
_10
import { add } from "./utils"

…the build tool has to resolve ./utils to find index.ts parse index.ts to process its export * statements. Locate math.ts, date.ts and validation.ts to determine what’s exported then finally, resolve add from math.ts.

This extra dependency chain adds overhead, especially when there are many export * statements or when there are barrel files are nested e.g. barrels importing other barrels.

A Barrels Effect On Test Time

#

At their core, barrel files act as a middleman between your tests and the actual code being tested. This layer of abstraction introduces additional steps in the process of resolving and loading modules, which can impact test performance and accuracy in several ways.

Similar to their effect on build time, barrel files can slow down tests by adding extra steps to resolve dependencies. For example, if your test imports a function from the barrel file. Your favourite test runner for example Jest or Vitest, needs to resolve the barrel file first, parse its re-exports, and then locate the actual source file.

This resolution overhead is minor for a single test but can add up when running hundreds or thousands of tests in a large codebase.

As we understand now, barrel files often include multiple exports via export *, some of which may import large libraries or unused modules. Even if your test doesn’t use these modules, the test runner may still process them, increasing the amount of memory uses and increases the test runtime unnecessarily.

Let’s paint a picture. Imagine we’re wanting to test a project with the following structure.


_10
/src
_10
└──App.tsx
_10
└──/utils
_10
├──├── math.ts
_10
├──├── string.ts
_10
├──└── index.ts
_10
└──/tests
_10
└──utils.test.ts

We have our various util modules…


_10
// src/utils/math.ts
_10
export const add = (a: number, b: number) => a + b;
_10
export const subtract = (a: number, b: number) => a - b;


_10
// src/utils/string.ts
_10
import lodash from 'loadash';
_10
export const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
_10
export const toLowerCase = (str: string) => lodash.trim(str.toLowerCase());

…which exist in our utils barrel file.


_10
// src/utils/index.ts
_10
export * from "./math";
_10
export * from "./string";

Finally we write our test code.


_10
// src/tests/utils.test.ts
_10
import { add, capitalize } from "../src/utils";
_10
_10
test("add function works correctly", () => {
_10
expect(add(2, 3)).toBe(5);
_10
});
_10
_10
test("capitalize function works correctly", () => {
_10
expect(capitalize("hello")).toBe("Hello");
_10
});

Let’s explore a few issues that arise.

By importing from the barrel file /src/utils the test will load all functions both math.ts and string.ts, even though it only uses one function from each file. string.ts imports a heavy library lodash, even though the function we need doesn’t require it and the test runtime increases unnecessarily.

If we decide to mock add in this test, you’re technically mocking it at the barrel level. Any other tests importing add from the barrel file might also be affected, leading to unintended side effects.

If one of these tests fails, it’s harder to pinpoint the source of the issue because the barrel file hides the direct relationship between the test and the source file.

Barrel files can obscure the relationship between your test and the actual module being tested, reducing test isolation. When a test fails, it may not always be clear which module or re-exported code caused the failure, making debugging more time-consuming.

A Barrels Effect On Circular Dependencies

#

Barrel files can inadvertently create or exacerbate circular dependencies, which occur when two or more modules depend on each other, directly or indirectly. While JavaScript and TypeScript can handle some circular dependencies without crashing, they often lead to unexpected bugs, broken imports, or runtime errors that are hard to debug.

In JavaScript, if a circular dependency is detected, one of the modules might be partially loaded or undefined. This can lead to runtime errors when you try to call a function or access a value that hasn’t been fully initialised.


_11
// utils/index.ts
_11
export * from "./fileA";
_11
export * from "./fileB";
_11
_11
// fileA.ts
_11
import { funcB } from "./index"; // Importing from the barrel file
_11
export const funcA = () => funcB();
_11
_11
// fileB.ts
_11
import { funcA } from "./index"; // Importing from the barrel file
_11
export const funcB = () => funcA();

Circular dependencies can cause inefficiencies in module resolution and increase build and runtime overhead, especially in large applications.

However there are things you can do mitigate this.

The eslint-plugin-import package includes a rule to detect circular imports.


_10
npm install eslint-plugin-import

Add the following rule to your ESLint configuration:


_10
"rules": {
_10
"import/no-cycle": "error"
_10
}

For typescript projects you can enable the --traceResolution flag in tsc to log how TypeScript resolves imports. This can help identify circular dependencies.

A Case For Barrel Files

#

There are things that barrel files allow us to do. Consolidate multiple imports from the same directory into a single import statement as was glamorously illustrated. Barrel files act as a hub for related modules which visually improve project structure and can enforces a consistent import pattern across the codebase, which is always really useful when you’re working in a team.

To get the most out of barrel files, follow these best practices.

Use Explicit Exports - Avoid export * unless absolutely necessary to prevent accidental imports of unused or unwanted code.

Combine with Index Files for Subdirectories - Use index files inside subdirectories to flatten imports:


_10
/Button
_10
├──Button.tsx
_10
├──Button.styles.ts
_10
└──index.ts

Analyze Dependencies - Use tools like Madge or ESLint to make sure you’re keeping on top of your dependency tree.

Conclusion

Barrel files are an invaluable tool for keeping your imports clean, your project organised and your development experience smooth, especially when working in a team. By consolidating exports, you can simplify the structure of your codebase, making it easier to scale and maintain.

Having only a handful of barrel files in your code is usually fine, but the more you use them, the more problematic things become. More code gets included, often unnecessarily, which can decrease the performance of many operations and in some cases can cause your code to run unreliably.

As always balance is the key and the best way to learn is to experiment. Try and find a way to make barrel files files work for you in your own apps!

Halpy codings,

DANNY

Reinforce Your Learning

One of the best ways to reinforce what your learning is to test yourself to solidify the knowlege in your memory.
Complete this 5 question quiz to see how much you remember.

1) What is the primary purpose of a barrel file in JavaScript/TypeScript?
file under:
Typescript

Thanks alot for your feedback!

The insights you share really help me with improving the quality of the content here.

If there's anything you would like to add, please send a message to:

[email protected]

Was this article this helpful?

D is for danny

About the author

Danny Engineering

A software engineer with a strong belief in human-centric design and driven by a deep empathy for users. Combining the latest technology with human values to build a better, more connected world.

Gobacktothetop

Made with 🥰 in 🏴󠁧󠁢󠁥󠁮󠁧󠁿

©2025 All rights reserved.