← All articles

Why I Prefer Barrel Files in 2026

(updated )

Barrel files have had a rough few years. If you have been reading JavaScript blogs, chances are you came across the argument that they hurt your bundle size, your dev server startup time, and occasionally your sanity. The most cited piece on the subject is TkDodo’s “Please Stop Using Barrel Files” from the end of 2024, which lays out the case pretty convincingly.

Personally, I never stopped using them. In my long-running projects I was already very invested by the time the criticism picked up steam, and restructuring my codebases around the absence of barrel files did not seem worth the effort. That does not mean it was painless. I spent a frustrating amount of time untangling circular imports after I first understood what was causing certain bundler errors, and the bundles were definitely larger than necessary. Luckily for my projects that didn’t really matter much. So I stuck with the pattern, and looking at where the tooling has landed now, I am glad I did, and I plan to keep using them in new codebases.

This article is my attempt to explain why I think most of the old arguments no longer apply, and why a barrel file is actually a useful thing to have in the places where it makes sense.

What is a Barrel File

Just in case you have somehow avoided this discussion, a barrel file is an index.ts that sits in a folder and re-exports things from the other files in that folder. The idea is that consumers can write import { Button, Card } from "~/components" instead of two separate imports from the individual files.

There are three main arguments people have made against them:

  1. They can create circular imports that crash bundlers with confusing errors.
  2. They slow down your dev server, because importing from a barrel loads all the modules it references.
  3. They defeat tree-shaking, so unused code ends up in your production bundle.

All three had teeth at the time. I think none of them holds up in 2026 for the way I use barrel files today.

The Circular Imports Argument

The classic failure mode goes like this. You have a folder components/ with button.tsx, card.tsx, and an index.ts re-exporting both. Inside button.tsx, your editor autocompletes an import for Card to come from ~/components (the barrel), not from ~/components/card directly. Now the barrel is pulling in button.tsx, which is pulling back from the barrel. The runtime allows this, but bundlers sometimes do not, and when they break, the error messages tend to make very little sense.

The fix is a lint rule. eslint-plugin-import has no-cycle, and you can additionally forbid a file from importing its own folder’s index using no-restricted-imports with a relative pattern. These rules are worth enabling regardless of whether you use barrel files.

Before coding agents, keeping these rules consistent took some discipline. Now the agent reads your lint config, respects it, and corrects itself if it slips. Most of the historical friction came from humans writing or navigating imports by hand, and that is not really the situation I find myself in anymore.

The Dev Performance Argument

This is the strongest point in the original article. TkDodo reports a Next.js project where removing internal barrel files reduced the number of modules loaded per page from about 11,000 to about 3,500. That is a significant difference, and 5 to 10 second page startups are painful.

The context matters though. That measurement was from a Webpack-based Next.js dev server, which walks the module graph eagerly. Pulling in a barrel pulls in every file the barrel touches, even the ones you did not actually use.

In 2026, that is not how most dev servers work anymore. Vite, Turbopack, Rolldown, and Rspack walk the module graph lazily, and in many cases do not compile the unused modules at all. That, more than anything, is what neutralizes the measurement. Next.js has also shipped optimizePackageImports to address a related problem. It specifically targets external npm packages with huge barrel exports like icon libraries, not the barrels in your own app code, but it is a signal that even Vercel considers barrel imports worth engineering around rather than banning outright.

If you are still on a legacy Webpack-based dev setup and your pages are slow, your problem is probably not the barrel files. It is the dev server. Upgrading is almost always easier than rearchitecting your imports.

The Tree-Shaking Argument

The concern was that a barrel like this:

// components/index.ts
export * from "./button";
export * from "./card";

makes it hard for bundlers to figure out what is actually used. It got worse when people wrote import * as Components from "~/components" in the consumer, because now you have lost all ability to reason about which symbols are touched.

Modern bundlers have mostly settled this. esbuild, Rolldown, Turbopack, and Rspack track symbol-level usage through both export * and named re-exports, and drop the unused ones reliably. The import * as X consumer pattern is still a problem, but that is a caller-side mistake, not inherent to barrels.

You can also write the re-exports by name if you want the barrel to double as an explicit statement of what the folder exposes:

// components/index.ts
export { Button } from "./button";
export { Card } from "./card";
export type { ButtonProps } from "./button";

For tree-shaking the two shapes are roughly equivalent. The case for naming is readability. The public surface is visible at the top of the file instead of implicit across the whole folder.

Agents Keep Barrels Up to Date

Possibly the biggest reason barrel files fell out of favor was not performance but maintenance. Adding a component meant updating the barrel. Moving something meant updating two files. Deleting something meant cleaning up the index, or risking a stale re-export that still compiled but pointed at nothing.

In 2026 I no longer update barrel files by hand. Coding agents handle this well, which turns the whole maintenance-cost argument against barrels into a non-issue.

What Barrel Files Actually Buy You

The code structure argument is the one that usually gets skipped in these debates, and I think it is the most interesting.

A folder with a barrel file has a public interface. Everything in the index is exposed. Everything else is implementation detail. That distinction is something TypeScript does not express natively in any other way. There are no public or private keywords on modules. A folder is just a folder, and by default every file in it is equally importable from anywhere.

A useful side effect is that internal refactors stay internal. When you restructure the files inside a module without changing its public surface, the diff contains the barrel and the moved files, and nothing else. Reviewers can focus on the actual change instead of scanning through dozens of consumer import updates to confirm that nothing snuck in beyond a path rename.

A concrete example. I often put a helpers/ subfolder inside a feature folder for little utility functions that only make sense in the context of that feature. These helpers are never part of the parent’s barrel. If somewhere else in the codebase I find an import like ~/features/checkout/helpers/parse-address, that import is visibly wrong. It reaches into implementation detail the module did not intend to expose. The long path itself is a smell that jumps out during review.

You can enforce this with eslint-plugin-import’s no-restricted-paths rule, which lets you express “nothing outside features/checkout may import from features/checkout/helpers/*.” Before agents, this was overkill for most codebases. Now you get the enforcement essentially for free. Without a barrel, there is no distinction between public and private files in a folder, so this whole category of violation has nowhere to land. You cannot lint what you cannot name.

There is another angle worth pointing out. Even in a workflow where your agent is writing most of the code, a human still has to read it. When every module is imported from its folder root, the dependency graph between parts of your codebase becomes much easier to see at a glance. Without barrels, imports like ~/features/checkout/components/form/submit-button bury the actual dependency relationship under layers of internal structure, and odd dependencies between parts of your code become harder to spot.

A Note on Rust

This idea is not new. In Rust, every module has a file where you use pub use to re-export exactly what the module intends to expose. Anything not re-exported is private to the module. This is not controversial. It is the recommended, idiomatic way to organize code.

When you argue against barrel files in TypeScript, you are essentially arguing against the idea that a folder should have a public interface. I think that idea is worth keeping, and a barrel file is the closest tool TypeScript gives us to express it.

Where I Do Not Use Barrels

I do not think every folder should have a barrel file.

I keep code colocated with the feature that uses it. A feature folder has its own components/, hooks/, helpers/ subfolders at whatever depth makes sense, and things get hoisted up the tree only when a second caller needs them. In that setup, the barrel at a feature’s root only exposes the things that genuinely escape. The large internal surface of the feature stays out of sight. That is exactly the shape modern bundlers handle best, and it is also where the barrel’s role as a public-interface declaration is clearest.

The objection to wide aggregator barrels (a top-level components/index.ts or features/index.ts that pulls in everything) really only applies when you organize code by kind rather than by domain. If every component lives flat at the top of components/, a barrel there will be huge. If components live inside the features that use them, the top-level folder does not need a barrel at all, and the feature-level barrels stay narrow by construction. Much of the bundler-perf argument against barrels is really an argument against kind-based layouts with wide roots, not against barrels themselves.

A top-level components/ folder is the case I am genuinely less certain about. A barrel there makes imports noticeably cleaner, and on modern bundlers the tree-shaking works correctly. But if you have a large shared components folder that is used on every page, and your framework does not have an equivalent of optimizePackageImports for your own code, you may still pay some dev-mode cost. I have not benchmarked this exhaustively. If you go this route, measure it on your own setup before committing.

A features/ folder containing subfolders for each independent feature does not need a barrel either. You are never going to import ten features from the same place. The same logic applies to a top-level lib/ folder, where I keep modules that could in principle be published as standalone npm packages. These subfolders tend to be unrelated to each other, and not having a barrel at the root makes it clear each subfolder is its own self-contained thing. Inside each one, there can still be a barrel that defines what that particular module exposes.

The same principle carries up one more level in a monorepo. For shared packages that bundle together several unrelated modules (a common shape for an internal @myorg/utils or @myorg/shared), I prefer multiple entry points via the exports field in package.json over a single root barrel. Consumers import from @myorg/shared/logger or @myorg/shared/date rather than from @myorg/shared, each entry point has its own barrel internally, and the root of the package does not have to pretend the pieces are related. It is the same “folders whose children are independent modules should not have a shared barrel” rule, just applied at the package boundary. It also sidesteps bundler edge cases that show up when a wide aggregator barrel is the only way in.

If you find yourself looking at a folder where the files really are unrelated to each other, I would take that as a sign to restructure rather than to reach for a barrel. A barrel does not make unrelated things related. It just hides the lack of cohesion behind a shared import path.

The general heuristic: a barrel belongs on a folder that behaves like a cohesive module with internal implementation. A feature folder with some files and a helpers/ subfolder fits. A folder whose children are independent modules does not.

A Note on Server Components

There is one case that deserves its own section, because it is the place where “folder as module” genuinely fractures.

In a React Server Components codebase, the server and client boundary cuts orthogonally to feature boundaries. If a folder mixes "use client" modules and server-only modules (anything that imports server-only or relies on Node-only APIs), a single shared barrel becomes hostile to both sides. Importing one symbol can transitively drag the opposing module into a context where it throws.

The “one folder, one public interface” framing that the rest of this article rests on assumes a single public interface is the right unit. In RSC, a feature often has two: one for the server side, one for the client side. I have not worked in an App Router codebase long enough to have a confident recommendation for how to shape barrels around this, but it is worth flagging that the default pattern does not carry over cleanly, and that any solution will need to treat the two sides as separate public surfaces rather than one.

It is worth pointing out that this is mostly a problem the directive-based model creates for itself. TanStack Start takes a different route on RSC and does not use "use server" at all. The boundary lives in the filename: .server.ts for server-only helpers, .functions.ts for createServerFn RPC wrappers, and plain .ts for isomorphic code. The .server.ts files are only reached through the .functions.ts wrappers, and those wrappers are safe to import from anywhere, because the build replaces their implementations with RPC stubs in client bundles.

In that setup the barrel story collapses back to a single public interface. The barrel exposes the .functions.ts wrappers and any client-safe modules; the raw .server.ts files stay out of it. They are private implementation reached through the wrappers, in the same “wide internal, narrow public” shape the rest of this article argues for. The suffix itself is doing the job I keep wishing TypeScript did natively: a file-level public/private declaration, just expressed by the framework rather than by a folder’s barrel. I have not shipped a large TanStack Start codebase either, so I will not commit to a recommendation, but it is worth flagging that the RSC-vs-barrels tension is not universal. It is specific to the directive-based model.

So, Are They Fine?

I think they are. Not for every folder, and not if you are using export * with a legacy Webpack dev server, but in most modern setups the barrel file is back to being what it was originally meant to be: a compact declaration of what a folder exposes.

The tools that made them painful have changed. Lint rules catch the circular imports. Modern bundlers handle the tree-shaking. Coding agents maintain the file for you. What remains is the thing barrel files were always good at: giving a folder a public interface.

If you would like to leave feedback or have questions about this article, feel free to contact me on Bluesky . You can also follow me there to find out when new articles are published.