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';
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
Then all consumers use the folder as if it were a package boundary:
import { User, Product, Order } from '@shared';
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';
is not always equivalent, from a tooling-cost perspective, to this import:
import { User } from '@shared/models/user.model';
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-adepends onuser.model
You get something closer to:
-
feature-adepends onshared -
sharedre-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;
}
// address.model.ts
import { formatName } from '@shared';
export interface Address {
city: string;
formattedBy?: string;
}
// shared/index.ts
export * from './user.model';
export * from './address.model';
export * from './format-name';
Now the graph becomes:
user.model -> address.model -> shared/index.ts -> user.model
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 undefinedCannot 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';
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';
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';
A barrel import tells a softer story:
import { User } from '@shared';
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';
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';
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';
// orders.component.ts
import { User, formatCurrency, StatusPipe } from '@shared';
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';
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
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';
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)