performance
patterns
barrel-exports
webpack
bundlers

Everything About Barrel Exports In JavaScript

If you've been coding in JavaScript for a while, you’ve probably encountered those annoying, repetitive import statements that pile up and clutter your code.

import { Sum } from 'libs/calculator/sum/Sum';
import { Add } from 'libs/calculator/add/Add';
import { Remove } from 'libs/calculator/remove/Remove';

We’ve all been there - importing functions or components one by one, and before you know it, the top of your file looks like a chaotic mess. That’s where the barrel exports pattern swoops in to save the day.

// We group exports to reduce boilerplate
export * from './sum/Sum';
export * from './remove/Remove';
export * from './add/Add';

// Importing
import { Sum, Add, Remove } from 'libs/calculations';

But, as with most things in JavaScript, nothing is ever as easy as it seems. While barrels can tidy up your imports and exports, they come with their own set of headaches - especially when it comes to performance, tree-shaking, and overuse.

The funniest part is that, visually, it seems like we've improved our code. We have fewer lines when importing things, and everything looks simpler.

However, that's a false assumption (we’ll learn why soon) - this is a common occurrence in the JavaScript ecosystem, where written code goes through thousands of tools that transform it into something else, and you’re often unaware of how it will look in the end (✿◡‿◡).

In this article, we’ll explore both the good and the bad sides of the barrel exports pattern. By the end, you’ll know how and when to use them, as well as understand common pitfalls to avoid.

Why Do Barrel Exports Have A Bad Reputation?

Sometimes developers behave impulsively, almost like kids, rather than like engineers. Every decision has consequences, and in the era of tech influencers, it's easy to internalize misleading negative statements. In my opinion, the criticism of barrel exports often comes from people who’ve never actually measured their impact. Most likely, they read an article saying, "It’s bad, don’t use it," and then followed that advice blindly.

It’s the same situation as when someone says, "Design patterns aren’t needed" or "Who cares about data structures and algorithms?". Sigh... yeah, people actually say things like that. And then, there are others - often less experienced - who believe it or take it as a golden rule.

But the reality is always more nuanced. Some projects will require both, some will require neither, and some may need just one. It’s the same with barrel exports. Depending on the case, they can be beneficial, unnecessary, or even problematic if used incorrectly.

So, when someone focuses on the negative aspects of something, it’s easy for many people to conclude that it’s not even worth considering. I’ve seen this countless times in my career.

First, there’s a big buzz around SPAs, then comes the backlash, with critics attacking the concept (even though SPAs still have plenty of valid use cases).

Next, there’s the love for Next, quickly followed by hate and exaggerated problems being blown out of proportion.

What about TDD or clean architecture? The same cycle repeats!

Before we go any further - forget everything you think you know about barrel exports, grab a big cup of coffee, and let’s take another look - calmly, using real engineering arguments and checks. Then we can draw conclusions and form our opinions based on facts and experience.

What Is The Barrel Exports Pattern?

The barrel exports pattern is a way to consolidate exports from multiple modules into a single file, typically named index.js or index.ts. This allows you to import everything you need from one place, making your imports cleaner and more manageable.

This pattern is not exclusive to JavaScript/TypeScript. Many languages, such as C#, Go, Java, and others, implement similar concepts. The approach may vary depending on the language. For example, here is a Python thread that demonstrates the idea.

Let’s say you have a few utility functions:

// utils/validate.js
export default function validate() {
    return 'validating...';
}

// utils/format.js
export default function format() {
    return 'formatting...';
}

Without using a barrel exports, your imports might look like this:

import { validate } from './utils/validate';
import { format } from './utils/format';

Now, let’s introduce the barrel exports. In the utils folder, you create an index.js file that exports everything:

// utils/index.js
export { default as validate } from './validate';
export { default as format } from './format';

This allows you to replace those two import lines with just one:

import { validate, format } from './utils';

Benefits Of Using Barrel Exports

Let’s break down the key advantages that barrel exports bring to codebase.

1. Cleaner Imports

Instead of cluttering your files with multiple lines of imports, you can consolidate everything into a single line.

import { Sum } from 'libs/calculator/sum/Sum';
import { Add } from 'libs/calculator/add/Add';
import { Remove } from 'libs/calculator/remove/Remove';
// vs
import { Sum, Add, Remove } from 'libs/calculator';

By reducing the number of import lines, your code becomes more readable and easier to maintain. Additionally, the import paths become shorter.

2. Better Folder Structure

Barrel exports help keep your folder structure organized. Grouping related files under a single entry point makes it easier to navigate and understand the layout of your codebase, especially as the project scales.

Structure With Barrel Library Structure With Barrel Exports

It’s immediately clear where the "starting point" of a module, library, or feature is, typically in the index.ts or index.js file.

3. Easier Refactoring And Structural Changes

When you need to refactor your project, barrels save you from manually updating multiple import paths. As long as the barrel file remains in the same location, the imports throughout your project won’t break, even if the internal file structure changes.

Structure Changed And No Refactor Needed

In the GIF, you can clearly see the benefit. The responsibility for updating exports lies with the library’s author, not the consumer. From the application’s perspective, nothing has changed. This is a significant advantage for the library consumer. Now, imagine the frustration for developers when, as the library author, you move to a completely different folder/file structure, and it forces the consumer to update imports across the entire app (✿◡‿◡).

Problems With Barrel Exports

While barrels are great for organizing imports, they can also create problems if used incorrectly/too much. Let’s take a look at some of the pitfalls we might encounter.

1. Unpredictable Tree-Shaking

Tree-shaking is an optimization that removes unused JavaScript code from your bundle, similar to how Tailwind removes unused CSS via PurgeCSS.

However, barrels can sometimes interfere with this process. If not handled properly, tree-shaking might fail to eliminate unused code from the exported modules, leading to a bloated bundle.

To illustrate this, let’s imagine we’re building a small calculation utilities library:

// logger.ts
export const logger = (...items: number[]) => {
  console.log(items.map((item) => `${item}\n`).join(`\n`));
};

// utils.ts
export const sum = (...items: number[]): number =>
  items.reduce((acc, item) => item + acc, 0);

export const min = (...items: number[]): number | null =>
  items.reduce((acc, item) => (item < acc ? item : acc), 0) || null;

export const max = (...items: number[]): number | null => {
  return items.reduce((acc, item) => (item > acc ? item : acc), 0) || null;
};

// index.ts
export * from './utils';
export * from './logger';

// app.ts
import { sum } from '@calculator';

Here’s the structure:

Calculator Folder

Now, when we build our files (I’m using Webpack5), we observe an interesting behavior. Even though I’ve imported only one function, the bundle size changes depending on the functions we import. Here’s the measured bundle size before and after importing each function:

Nonesumsum + minsum + min + maxsum + min + max + logger
841.63 kB842.63 kB843.22 kB844.01 kB845.12 kB

This shows that tree-shaking works correctly in this case. However, this is because I’m using modern Webpack 5 with a professional configuration from the Gatsby team. I can’t guarantee the same results with your Webpack setup, especially if you’re using a custom configuration or an older version.

Additionally, if you’re using a different bundler, the process may vary - it’s not always as simple and straightforward.

Measurements were conducted on the 4markdown repository.

2. Blurred Dependencies And Readability

While barrels can simplify imports, overusing them can obscure dependencies. When too many unrelated modules are combined in a barrel, it becomes difficult to track what’s actually being imported.

// Inside index.ts
export * from './test-utils';

// Inside test-utils.ts
export * from './fixtures';
export * from './setup-env';

// Inside setup-env.ts
export * from './defs';

As a result, when working with deeply nested files in the repository, you might accidentally export something unnecessary for the module consumer, increasing the bundle size - as discussed earlier.

Additionally, debugging and working with such code can be very frustrating. While the abundance of index.ts files can be filtered by path in searches, it still makes the code harder to navigate and manage effectively.

Overused Barrel Exports

So, as you can see, only the path or parent folder name gives you an understanding of which index.ts file you might be looking for.

3. Sneaky Circular Dependencies

Circular dependencies occur when two or more modules depend on each other, causing issues like infinite loops or other unintended behavior. Barrels can unintentionally introduce these circular dependencies without you even realizing it.

Imagine this scenario: someone says, "Do you want to buy something for $10?". Naturally, you'd ask, "Okay, but what are you selling?". And they respond with, "You’ll see" ☜(゚ヮ゚☜).

Barrel exports can feel like these "mystery boxes" - making it easy to unknowingly create circular dependencies. For example, you might encounter a direct loop like A → B → A, or a more complex one like A → B → C → D → E → F → G → B.

Sneaky Circular Dependencies Are Problematic

In this case, each letter represents a file. While you can reduce the risk of circular dependencies by using static code analysis or tools like NX to detect and visualize them, there’s still a chance they’ll go unnoticed until they cause problems. Tools like NX can generate a dependency diagram and raise alerts in CI/CD pipelines, but circular dependencies often remain hidden until they break something or bloat your bundle size.

Barrel Exports In TypeScript

When working with TypeScript type definitions, it’s quite common to use barrels to enjoy their benefits. With TypeScript, the situation is a bit different from JavaScript modules. Since TypeScript has zero impact on the final bundle - it's purely for type checking - you don’t have to worry about the issues mentioned earlier, such as tree-shaking or circular dependencies. Therefore, using barrels in a type definition library is perfectly fine.

Take a look at the example below:

Barrel For TypeScript Barrel For TypeScript Definitions

As long as you're not using features like enum or other JavaScript-related code (real logic), you have nothing to worry about. Barrels work smoothly in TypeScript without any performance or bundling concerns.

To understand why enum generates JavaScript, read the article Ugly Relationship Between Tuples in TypeScript and JavaScript.

Barrel Exports In Libraries

Here’s where things get tricky, especially if you're using export * from './path'. Not using barrels in your library’s codebase can frustrate your consumers. As I mentioned earlier, if you change the internal structure of your library, your consumers will need to update their import paths, which can cause unnecessary hassle.

For libraries, I would say barrel exports are essential to make both your life and your consumers’ lives easier.

However, when using barrels, here’s a tip: instead of blindly re-exporting everything, explicitly define what you want to export. This will simplify things for the bundler and give you more control over what is being exported.

export * from './validators';
// vs
export { min, max, maxLength } from './validators';

Additionally, instead of using one-line exports for everything, consider exporting at the bottom of the file. While this can slightly increase boilerplate, it improves readability and gives you more control over what is shared with consumers.

export const sum = () => {};
export const add = () => {};
// vs
const sum = () => {};
const add = () => {};

export { sum, add };

By following these two practices, you reduce the risk of re-exporting unnecessary code. If you need to share a utility internally within the library, it won’t be exposed to the consumer by default. This helps keep your public API clean and ensures only the required functionality is available.

Using The Type Keyword In Imports And Exports

By using the type keyword in imports and exports, you can reduce your bundle size in certain cases.

I’ve explained this in more detail in the article Consider Using Type Imports In TypeScript.

Here’s how you can apply it:

// Barrels
export { min, max, maxLength, type MyType } from './some-mixed-ts-js-code';
// or
export type * from './full-ts-defs-file';

// Usage
import type { MyType, MyOtherTypeFromFullTSDefsFile } from '@library';
// or
import { type MyType, maxLength } from '@library';

By explicitly marking types with the type keyword, you ensure that only the types are imported or exported. This helps reduce bundle size, makes tree-shaking more predictable, and improves readability - you immediately know what is a type and what is not.

Barrel Exports In An Application Codebase

Seeing the benefits of barrel exports might tempt you to use this approach throughout your application codebase. However, this is a big mistake. While there are some areas where barrels might make sense - such as in folders containing utility functions - overusing them can quickly lead to your entire project being organized as a barrel, which is far from ideal.

Barrel exports aren’t a great fit for application codebases that deal heavily with domain-specific logic. Additionally, app code is often tightly coupled, which increases the risk of unintended behavior with tree-shaking. If you find your import paths getting lengthy, don’t default to barrels - use them only when there’s a clear benefit.

No Forced Barrels No Forced Barrels

That said, if you have a folder or related code in your application that’s closely connected by logic or purpose - like a set of calculator functions spread across multiple files for different operations - using barrels might be worth it.

In contrast, the previous example shows totally different components that perform unrelated tasks. Re-exporting them in a barrel will likely bloat your bundle and slow down builds.

Here Barrels make sense Case Where Barrels Make Sense

So why do barrels make sense for the calculator or in api-4markdown? It’s because the code, although split into multiple files, implements a single feature or serves a single purpose - such as handling communication with the backend.

In contrast, the earlier example with React components lacks a unifying purpose. The only commonality is the file names or categories - they’re all React components, but they serve different roles. Grouping them in a barrel would cause unnecessary bloat.

Barrels In Monorepos And Large-Scale Projects

Large-scale projects are typically divided into separate libraries with low coupling, where communication is often handled through patterns like the observer pattern or via shared type definitions. Instead of importing other libraries directly, you can pass an interface to functions, which helps to reduce coupling.

It’s not just about libraries; many large systems are divided into separate applications, such as one for admins and another for end-users. Some teams may even adopt a Micro Frontends architecture.

With so many libraries and shared code between apps, developers naturally lean toward using barrel exports. This approach is perfectly fine as long as you avoid the mistake of using barrels in places where there is no logical correlation between the files.

Barrels In Monorepo Barrel Exports In A Large Monorepo

As you've seen, I use barrels in library code, but I also apply them in applications when there’s a directory that contains shared first-class utilities.

Deeply Nested Barrels And Barrels Hell

If you have deeply nested barrels, where you’re re-exporting from multiple index.js/ts files, avoid using export * from './anything'. This can quickly lead to chaos and make your codebase hard to read. Instead, re-export only what truly needs to be exposed. Also, avoid creating unnecessary index.ts files, or you’ll end up with something like this:

Overused Barrel Exports And Madness Barrel Hell

This is one of the worst, most counter-productive approaches you can take. It makes the codebase incredibly difficult to work with and is a maintenance nightmare. Avoid falling into this trap.

If you see a project structured like this, be prepared to waste a lot of time:

components/
  ├── Button/
  │   └── index.ts
  ├── Header/
  │   └── index.ts
  ├── Footer/
  │   └── index.ts
  ├── Sidebar/
  │   └── index.ts
  └── index.ts

All Is About Measurements And Trade-Offs

If your project doesn’t have any issues, why introduce patterns that solve problems you don’t have? This is exactly what happens in cases like the barrel hell I mentioned earlier.

You need to understand whether your code changes - structural or logical - impact your application’s size or performance. That’s why it’s crucial to have measurements integrated into your CI/CD process. These checks can compare performance and file sizes between previous and current versions.

Comparison Example Comparison Example Of Application Files

For this, you can use tools like Unlighthouse and some custom scripts. While I don’t have space in this article to provide an implementation, the key takeaway is: measure, analyze, spot problems, and only then introduce a pattern. That’s the golden rule!

Here's an article on how overusing design patterns can become problematic.

When Do Barrel Exports Provide Value?

  1. Type Definition Libraries – safe to use, even with deep nesting.
  2. When using export type and import type for type-only imports and exports.
  3. In library codebases – maintaining clean, manageable imports.
  4. In application codebases, where there is a clear logical correlation between files.
  5. When used strategically, considering the pros and cons.
  6. In large projects with multiple separated libraries or modules.
  7. When you have performance and build benchmarks to monitor and optimize.
  8. When the barrel candidate’s code is frequently imported across the project.
  9. When using a modern bundler configured to handle tree-shaking efficiently.

When Not To Use Barrels?

  1. When they are forced or overused in places where they don’t fit.
  2. When used for codebases lacking a logical connection between files.
  3. When they negatively impact performance (always ensure you measure and assess the impact).
  4. When you’re uncertain about your bundler configuration or its effectiveness.
  5. For application codebases (with some exceptions, as mentioned earlier).

Summary

This is a topic that can be a double-edged sword. For libraries or type definitions, barrel exports seem logical and effective. Modern syntax that allows bundlers to differentiate between JavaScript code and type definitions makes our work easier and the build process more predictable.

However, overusing this approach comes with risks, such as hidden bundle size costs or the need for careful measurements to ensure it doesn’t cause issues, especially in an application codebase.

By now, you should have a clear understanding of when it’s worth using barrel exports and when it’s not. Ultimately, it should feel natural in your daily workflow - if it doesn’t, it’s probably not the right place for barrels!

Author avatar
About Authorpolubis

👋 Hi there! My name is Adrian, and I've been programming for almost 7 years 💻. I love TDD, monorepo, AI, design patterns, architectural patterns, and all aspects related to creating modern and scalable solutions 🧠.