DEV Community

Cristian Sifuentes
Cristian Sifuentes

Posted on

Why I Don’t Use Barrel Files (`index.ts`) in 2026

Why I Don’t Use Barrel Files ( raw `index.ts` endraw ) in 2026

Why I Don’t Use Barrel Files (index.ts) in 2026

Tags: angular typescript architecture performance testing

Why I Don’t Use Barrel Files (index.ts) in 2026

We all love a clean import.

// The Dream
import { User, Product, Order } from '@shared';
Enter fullscreen mode Exit fullscreen mode

It looks elegant. It hides the directory structure. It makes the code feel curated.

And for years, that aesthetic trick convinced a lot of teams that barrel files were a mark of maturity.

But in 2026, after working on large Angular applications, Nx workspaces, test-heavy codebases, and performance-sensitive CI pipelines, I have the opposite opinion:

Most internal barrel files are a tax.

A tax on startup time.
A tax on graph complexity.
A tax on test isolation.
A tax on debugging.
A tax on architectural clarity.

This article is not a theoretical rant against index.ts.
It is a practical argument from build behavior, dependency analysis, and real Angular maintenance pain.

Because the truth is simple:

A pretty import line is not worth a slower system.


TL;DR

Barrel files look clean, but in real Angular applications they often:

  • force bundlers and IDEs to parse more files than needed
  • make circular dependencies easier to create and harder to detect
  • slow Jest and Vitest startup by loading entire export surfaces
  • blur dependency direction inside feature folders and shared libraries
  • hide architectural smells behind “nice” imports

My rule in 2026 is straightforward:

  • For published libraries: use barrels for the public API
  • For internal app code: prefer direct imports

That one distinction removes a surprising amount of friction.


The Seduction of the Barrel

A barrel file usually starts as a convenience layer.

// shared/index.ts
export * from './user.model';
export * from './product.model';
export * from './order.model';
// ... 50 other exports
Enter fullscreen mode Exit fullscreen mode

Then all consumers use the folder as if it were a package boundary:

import { User, Product, Order } from '@shared';
Enter fullscreen mode Exit fullscreen mode

At first, this feels harmless.
In a tiny codebase, it often is.

But Angular applications do not stay tiny.
They accumulate feature areas, route-level boundaries, internal utilities, domain models, shared UI abstractions, test helpers, and secondary dependencies. Once that happens, the barrel stops being a shortcut and starts becoming an indirection layer the toolchain must keep paying for.

That cost is usually invisible until the project becomes large enough that every extra parse, every extra edge in the dependency graph, and every extra transitive load starts to show up in developer experience.

The editor becomes a little slower.
Tests take a little longer to boot.
Incremental rebuilds touch more nodes than expected.
Circular dependencies appear in places that seem impossible.

And all of that starts from a file that was supposed to make things “clean.”


1. The Tree-Shaking Lie

One of the most common arguments in favor of barrels sounds like this:

“Modern bundlers are smart. They’ll tree-shake everything we don’t use.”

That sentence is directionally true in a narrow production-build sense, but dangerously misleading in day-to-day development.

Tree-shaking is not magic.
Before a bundler can eliminate unused exports, it must still:

  • resolve the import target
  • parse the barrel file
  • inspect the export graph
  • traverse referenced modules
  • determine side-effect safety
  • build enough semantic knowledge to know what can be discarded

That means this import:

import { User } from '@shared';
Enter fullscreen mode Exit fullscreen mode

is not always equivalent, from a tooling-cost perspective, to this import:

import { User } from '@shared/models/user.model';
Enter fullscreen mode Exit fullscreen mode

The first version often requires the compiler, IDE, and bundler to walk through an aggregation surface that may expose dozens or hundreds of files.
The second version points directly at the one file the consumer wants.

That difference matters.

Why Angular Monorepos Feel This More

In Angular monorepos, especially with Nx or other graph-aware tooling, barrel files tend to inflate dependency analysis.
A single barrel import can create the appearance of a much wider coupling surface than the consumer actually needs.

Instead of:

  • feature-a depends on user.model

You get something closer to:

  • feature-a depends on shared
  • shared re-exports 100 files
  • tooling must reason across the full export boundary

Even when the final production bundle eliminates unused code, the analysis cost still happened.
And developer time is spent in analysis far more often than in final release packaging.

The Real Cost Is Front-Loaded

This is why barrel files are so deceptive.
They often do not fail dramatically in production.
They just quietly degrade:

  • local dev server startup
  • HMR responsiveness
  • TS language service responsiveness
  • incremental builds
  • test bootstrap time

That is a terrible kind of problem because teams normalize it.
They start saying things like:

  • “The workspace is just big.”
  • “Angular is a little heavy.”
  • “Vitest is fast, but this repo is weird.”
  • “Nx graphs are always noisy.”

Sometimes the real cause is far more mundane:

the dependency surface has been artificially widened by barrels.


2. The Circular Dependency Nightmare

This is the part that breaks teams emotionally.

Circular dependencies are already unpleasant when they are obvious.
With barrels, they become subtle.
And subtle cycles are the worst cycles.

Imagine this shape:

// user.model.ts
import { Address } from './address.model';

export interface User {
  name: string;
  address: Address;
}
Enter fullscreen mode Exit fullscreen mode
// address.model.ts
import { formatName } from '@shared';

export interface Address {
  city: string;
  formattedBy?: string;
}
Enter fullscreen mode Exit fullscreen mode
// shared/index.ts
export * from './user.model';
export * from './address.model';
export * from './format-name';
Enter fullscreen mode Exit fullscreen mode

Now the graph becomes:

user.model -> address.model -> shared/index.ts -> user.model
Enter fullscreen mode Exit fullscreen mode

That is a cycle.
But it does not look like a cycle while you are writing it.

That is the danger.

A barrel file collapses visibility.
It hides the actual direction of imports behind a single public facade. So the developer no longer sees the real edge they are adding. They think they are importing “from shared,” but they are actually importing from a graph that may point back into the current feature.

Why These Errors Are So Annoying

When this breaks, the failures rarely say:

“Your barrel re-exported a symbol that fed a cycle through address.model.”

You get things like:

  • TypeError: Cannot read properties of undefined
  • Cannot access 'X' before initialization
  • Angular warnings about circular dependencies
  • bizarre test-order failures
  • one environment working while another fails

That is why barrel-file cycles are such morale killers.
They are not only architectural problems. They are debugging traps.

Developers waste hours moving imports around, reordering exports, renaming files, or suspecting framework bugs, when the real issue is that dependency direction was hidden by an aggregation layer.

Direct Imports Make Cycles Easier to See

With deep imports, the same code becomes more honest.

import { formatName } from '@shared/utils/format-name';
Enter fullscreen mode Exit fullscreen mode

Now the dependency is explicit.
You can reason about it immediately.
If the imported file is in the wrong layer, you see the design smell faster.

Barrels do not create every cycle.
But they make cycles easier to introduce and harder to perceive.
That is enough reason for me to avoid them inside application code.


3. Jest and Vitest Performance: The Hidden Casualty

The second place barrel files become painfully visible is testing.

Unit tests are supposed to be narrow.
That is the point.
A test for UserComponent should load the smallest relevant surface area possible.

But barrel imports often sabotage that goal.

Imagine this seemingly harmless line inside a test subject:

import { formatCurrency } from '@shared';
Enter fullscreen mode Exit fullscreen mode

If @shared/index.ts re-exports dozens of helpers, models, tokens, pipes, adapters, and third-party wrappers, then your test environment may end up resolving far more than the test actually needs.

In the worst cases, one barrel drags in:

  • charting adapters
  • PDF generation wrappers
  • date libraries
  • analytics helpers
  • browser-only code
  • side-effectful registration files

That makes the cost of a “small” unit test much less small.

Why This Hurts More in Test Runners

Test runners do a huge amount of module graph work up front.
If your imports are explicit, the graph is narrow.
If your imports are barrel-based, the graph becomes broader and more ambiguous.

That shows up as:

  • slower cold starts
  • slower watch-mode updates
  • more mocking complexity
  • more brittle isolation
  • harder-to-explain failures when unrelated exports change

I have seen codebases where deleting internal index.ts files and switching to direct imports produced visibly faster test startup with no functional change in the app itself.

Not a small theoretical gain.
A developer-notices-it-immediately gain.

And that is the type of optimization I care about most: the one that improves feedback loops.

Isolation Matters More Than Import Beauty

A test suite should pay only for what it exercises.
That is one of the cleanest design principles in engineering.

Barrel files violate that principle by turning “what I exercise” into “what this folder happens to export.”

That is not isolation.
That is convenience masquerading as architecture.


4. Barrel Files Blur Architectural Boundaries

This is the part many teams underestimate.

A deep import tells the truth about where a dependency lives:

import { User } from '@shared/models/user.model';
Enter fullscreen mode Exit fullscreen mode

A barrel import tells a softer story:

import { User } from '@shared';
Enter fullscreen mode Exit fullscreen mode

The second version feels more elegant, but it also weakens architectural literacy.
It trains developers to think in terms of broad shared buckets instead of real boundaries.

Over time, this has consequences.

A developer stops asking:

  • Is this model actually part of shared domain?
  • Should this utility live in feature scope?
  • Am I importing from the right layer?
  • Why is this component aware of this type at all?

Because the barrel has flattened all those questions into a single alias.

The Import Line Stops Teaching

One underrated feature of explicit imports is that they teach the codebase.
They show:

  • ownership
  • folder intent
  • dependency direction
  • domain placement
  • cross-feature coupling

When every symbol comes from a single barrel, that educational signal disappears.
And in large teams, that matters a lot.

Architecture is not only what is true.
It is also what is visible.

Barrel files reduce visibility.
And reduced visibility usually leads to accidental coupling.


5. IDE Performance and Autocomplete Are Not Free

This point sounds minor until you work inside a very large workspace.

VS Code, TypeScript Server, and other language tools must resolve symbols, navigate definitions, build import suggestions, and recalculate references constantly.
When many internal imports flow through barrels, the editor has to do more indirection work.

That can affect:

  • go-to-definition speed
  • rename refactors
  • auto-import quality
  • symbol indexing
  • memory usage in the language service

A direct file import is straightforward.
A barrel import requires the editor to identify the barrel, inspect its re-exports, locate the real file, and sometimes traverse nested re-export chains.

One layer of indirection is not catastrophic.
Hundreds of them across a workspace absolutely can be.

This is why many teams describe their editor as “a little sticky” in large TypeScript repos.
The source is often not Angular itself.
It is how the import graph has been abstracted.


6. The Most Misleading Part: Barrel Files Feel Professional

This is why smart teams keep them longer than they should.

They look mature.
They resemble package APIs.
They make folders feel curated.
And if you came from library authoring, they seem natural.

But internal application code is not the same thing as a published package.
That distinction matters.

A public package should present a stable entry point.
That is a real API contract.
Consumers should not know or care where files live internally.

For that case, a barrel is not only acceptable. It is correct.

Example:

// public-api.ts
export * from './lib/button/button.component';
export * from './lib/card/card.component';
export * from './lib/modal/modal.service';
Enter fullscreen mode Exit fullscreen mode

That is a library boundary.
That is intentional.
That is an API surface.

But inside your app, most folders are not public products.
They are implementation details.
Treating every internal folder like a published package usually adds ceremony without adding value.

My Rule in 2026

I use barrel files only when the folder represents a true public boundary.

That means:

  • published NPM packages
  • intentionally exposed secondary entry points
  • library-level public APIs

I do not use them for:

  • feature internals
  • shared app folders
  • component directories
  • utility folders
  • model folders
  • test helpers

Because those are not public contracts.
They are internal implementation surfaces.
And internal implementation surfaces benefit more from explicitness than from polish.


7. What I Prefer Instead: Deep Imports

Yes, deep imports are uglier.
Let’s say that honestly.

import { User } from '@shared/models/user.model';
import { Product } from '@shared/models/product.model';
Enter fullscreen mode Exit fullscreen mode

No one prints that on a T-shirt.
It is not cute.
It is not aspirational.

But it is operationally better in many Angular applications.

What Deep Imports Buy You
1. Zero ambiguity

You import exactly what you need, from exactly where it lives.

2. Fewer graph surprises

The dependency edge is explicit, narrow, and easier for tooling to reason about.

3. Easier cycle detection

You can see architectural direction immediately.

4. Better test isolation

The test runner loads what is required, not what a barrel happened to aggregate.

5. Stronger architectural clarity

The code shows true ownership and layer placement.

6. More honest refactoring

When imports are explicit, moving code forces you to confront boundary decisions instead of hiding them behind a facade.

That is the kind of friction I actually want.
Helpful friction.
Architectural friction.
The kind that prevents bad structure from scaling silently.


8. A Realistic Before-and-After Example

Here is the version many teams start with:

// shared/index.ts
export * from './models/user.model';
export * from './models/product.model';
export * from './models/order.model';
export * from './utils/currency';
export * from './utils/date';
export * from './pipes/status.pipe';
export * from './tokens/api.tokens';
Enter fullscreen mode Exit fullscreen mode
// orders.component.ts
import { User, formatCurrency, StatusPipe } from '@shared';
Enter fullscreen mode Exit fullscreen mode

That import is visually tidy, but semantically broad.
It says nothing about which layer is being used, which file owns the utility, or whether the component is depending on too much.

Now compare it with this:

import { User } from '@shared/models/user.model';
import { formatCurrency } from '@shared/utils/currency';
import { StatusPipe } from '@shared/pipes/status.pipe';
Enter fullscreen mode Exit fullscreen mode

This version is longer, but it is also more precise.
If someone reviewing the code sees a component importing multiple low-level utilities, a pipe, and a domain model, that review now has real architectural signal.

The import list becomes documentation.


9. When Barrel Files Are Especially Dangerous in Angular

There are a few contexts where I think internal barrels are particularly risky.

Shared folders that became junk drawers

If your project has a folder named shared and an index.ts exporting “everything useful,” you no longer have a shared module. You have a dependency fog machine.

UI libraries inside a monorepo with mixed concerns

If presentation components, tokens, domain models, config objects, and utilities all export through the same barrel, coupling becomes invisible.

Feature folders that re-export subfeatures

This often makes lazy boundaries leak. A feature should expose what the architecture intends, not everything its tree contains.

Test utilities mixed with production utilities

If a barrel re-exports both, test runners and editors pay unnecessary overhead, and production code may accidentally consume test-only helpers.

Route-level and domain-level code in the same alias

This encourages imports that violate intended separation between app shell, feature boundaries, and domain ownership.

These are not hypothetical mistakes.
These are common patterns in Angular teams that started with good intentions and ended with a flattened graph.


10. What About Developer Experience?

This is the strongest argument from the other side.

People say:

“But deep imports are annoying.”

Yes, sometimes they are.

But I think developers often optimize the wrong part of DX.

They optimize:

  • how short the import looks
  • how nice the path alias feels
  • how “clean” the top of the file appears

While ignoring:

  • test startup speed
  • incremental compilation performance
  • graph clarity
  • cycle debugging cost
  • refactor confidence

That is backward.

Real developer experience is not how pretty one line looks.
Real developer experience is how quickly and safely a team can understand, build, test, and change the system.

By that standard, direct imports often win.


11. My Practical Recommendation for Angular Teams

If you want a rule that is simple enough to enforce and nuanced enough to be useful, use this one:

Use barrel files only at true public boundaries

That means:

Good:
- library public API
- package entry points
- intentional external consumer surfaces

Avoid:
- internal feature folders
- shared app internals
- utils directories
- model directories
- component folders
- internal test support folders
Enter fullscreen mode Exit fullscreen mode

And if your repo already uses barrels everywhere, do not rewrite the whole workspace in one week.
Do it surgically.

Start with the worst offenders

Open the folders that are most likely to be causing pain:

  • shared/
  • utils/
  • core/helpers/
  • common/
  • giant feature barrels

Delete the index.ts.
Fix imports with auto-imports and path aliases.
Then measure:

  • dev server start time
  • test cold start time
  • watch mode responsiveness
  • TS server responsiveness
  • dependency graph readability

You do not need ideological proof.
You need operational proof.


12. The Architectural Principle Behind All of This

This article is not really about index.ts.

It is about something deeper:

Abstractions should clarify dependency direction, not obscure it.

A barrel file is useful when it represents an intentional public contract.
It is harmful when it turns internal implementation structure into an anonymous export bucket.

That is the dividing line.

I am not against convenience.
I am against convenience that makes systems slower, blurrier, and harder to reason about.

And in many Angular applications, that is exactly what internal barrel files do.


Final Takeaway

The dream import is attractive:

import { User, Product, Order } from '@shared';
Enter fullscreen mode Exit fullscreen mode

But dreams are cheap.
Tooling cost is not.

In 2026, I would rather have:

  • explicit dependencies
  • narrower graphs
  • faster tests
  • fewer circular dependency traps
  • better refactors
  • more honest architecture

than one line of aesthetically pleasing indirection.

So yes, my import statements are a little uglier now.

But my builds are clearer.
My tests are leaner.
My cycles are rarer.
And my codebase tells the truth more often.

That is a trade I will make every time.


The Rule I Actually Follow

Use barrel files for library boundaries.
Avoid them for internal application code.

If you are publishing @my-org/ui, expose a clean public API.
If you are inside apps/customer-portal/src/app/shared/utils, import the actual file.

That is the compromise.
And in my experience, it is the right one.


Next Step

Open your largest internal shared or utils folder.
Delete the index.ts.
Fix the imports.
Run the tests.
Start the dev server.

Then decide with measurements instead of tradition.

My bet is simple:

You will not miss the barrel nearly as much as you expected.


Written by Cristian Sifuentes

Angular engineer · Architecture-minded frontend developer · Performance-first systems thinker

Top comments (0)