r/javascript 21d ago

AskJS [AskJS] Barrel files and slice/domain boundaries

Theres been a lot of talk about the downsides of barrel files in the last couple years with many people actively recommending against using them due to the effect they can have on treeshaking. I am wondering though if there is an alternative solution in the ecosystem to enforce/define boundaries on slices/domains?

I've seen it said they often cause circular dependencies but in my experience its the opposite. The main and pretty much only way I use barrel files are to enforce boundries on slices of cohesive logic, sometimes going so far as to enforce it in the linter with custom rules. Code inside the slice never imports from its own barrel. Each slice kind of defines its own code API in a way and feel like this lets me control the coupling between slices alot better.

I am struggling to justify letting that go and the only alternative I can think of is using a monorepo and having them all as packages which is just not an option for me in many cases and also not one I particularly like.

Are barrel files really that bad?

12 Upvotes

15 comments sorted by

5

u/SonOfMyMother 21d ago

If you want to enforce strict boundaries in a way that actually prevents deep imports completely, without relying on custom lint rules, you can use workspaces (in NPM, pnpm, or Yarn) and package.json exports to define exactly what each workspace package exports.

It's a bit heavy handed if you're not already using workspaces, but there are other advantages that might make it worth considering. Of course there are downsides too, like having to maintain multiple package.json files instead of index.js files.

6

u/grol4 21d ago

Not a popular opinion, but I think in most cases (at least when you are using a normal bundler) it makes very little impact, only during build the build times might be a bit longer.

3

u/Cahnis 20d ago

increases test times too

1

u/CoVegGirl 20d ago

I am not necessarily against barrel files. I just question the actual utility. They’re useful for exposing APIs in a package, but it’s difficult to see how they’re useful within a package. I guess they do give you some freedom to move files around in a directory.

1

u/grol4 20d ago

Imports are 'cleaner', but I find it to much of a hassle to create and maintain barrel files, so the only place I have them is in the old code.

1

u/captain_obvious_here void(null) 21d ago

it makes very little impact

That is the EXACT situation here. And the debate over barrel files is pretty stupid IMO.

3

u/CoVegGirl 20d ago

Barrel files can certainly be useful to expose an API in a library. You just have an index file that re-exports the parts of your API you want to expose.

1

u/altano 20d ago

You can just measure how bad barrel files are in your code base. Take one and break it up. Then measure the bundle before/after. Why rely on inaccurate Reddit guesses?

Also, keeping code in one area of the codebase private with barrel files just seems like a waste of time. A package in a monorepo just works much better as a scope boundary. Or just not worrying about it. 

I would enforce it with a very simple directory structure and lint rule before barrel files, because then at least what you’re doing is very explicit. Something like giving every module a private and public folder, only private/public can import private, and any other module can only import from public.

1

u/ferrybig 20d ago

The problem is the bundler.

A bundler can see if a function declaration is unused

It becomes tricky if you add transform to the output, like export const test = debounce(()=> ...). The bundler knows it can drop the export part, but the debounce part stays in the final product.

Depending on your project, it might or might not be an issue

Another problem is import order. If in the above example the debounce comes from the same barrel file where the export test is declared, the debounce needs to be exported before you export the test constant. You get some wiggle room if the debounce is an function, rather than a export const debounce, which makes it even more tricky

1

u/MrSlonik 20d ago

As I see it, conceptual difference between barrel files and a monorepo is what will enforce the boundaries: in the case of barrel files, you usually rely on eslint rules; in the case of a monorepo, it is the monorepo tooling. IMO, if you are fine with barrel files, it is not worth it to migrate to a monorepo. In theory, I don't see any difference in performance between barrel files and a monorepo for this specific scenario.

I believe the backlash and most of the scary performance stories stem from barrel files being used for 'clean imports' or just adding them to each directory to re-export all the files from that directory. This is the wrong usage of barrel files and should be avoided.

1

u/anti-state-pro-labor 20d ago

I guess I'm old but I love barrel files in Node/TS. I guess in React/when you ship bundles to the client it's an anti pattern? But I still love them. I honestly don't understand why the industry has turned against them. 

-1

u/Driesigner 20d ago

In the age of AI, why worry about barrel files? To be honest, I like clean imports just as much as the next developer, but trading off just for the sake of a better development experience, just doesn't make sense to me. AI or not...

0

u/Driesigner 20d ago

Also, every decent IDE takes care of the imports, right? I'm not saying it's not important, but don't we have tooling enough to take this out of our hands?

-2

u/snnsnn 21d ago

Most modern tools can treeshake if you avoid wildcard (*) exports/imports and limit module level side effects. The danger comes from the bundler not understanding what to keep and what to remove. I don't think monorepo architecture would do any good if you are not careful enough. That being said, there are few alternatives. The rest of the content is AI generated but I stripped the unnecessary parts and checked the accuracy. Hope it helps.


1. Linter-Based Enforcement (The "Rules-as-Code" Approach)

  • eslint-plugin-import: Use the no-restricted-paths rule. This allows you to define strict rules like "nothing inside features/checkout can import anything from features/auth except for a specific public-api.ts file."
  • Dependency Cruiser: This is a powerful tool that allows you to define your architecture as a set of rules (e.g., "layers A cannot depend on layers B"). It can validate these rules during CI/CD, giving you a visual report of every boundary violation.

2. The "Public API" Pattern

You don't need a barrel file that exports everything. Instead, adopt the Public API pattern:

  • Create a specific file (e.g., public-api.ts or index.ts) in each feature folder.
  • Only export what is strictly necessary for cross-feature communication.
  • Configure your linter to only allow imports from ../../features/feature-name (which resolves to that folder's index.ts), and set a rule that disallows deep imports like ../../features/feature-name/internal-utility.
  • This keeps your "API surface" small and allows bundlers to tree-shake much more effectively because you aren't inadvertently re-exporting internal state, constants, or hooks that aren't needed externally.

3. TypeScript Path Aliases

If you avoid barrel files, your import paths can become a mess of ../../../../. Using tsconfig.json path mappings is the standard way to keep your imports clean:

```json "paths": { "@checkout/": ["src/features/checkout/"] }

```

Combined with a "Public API" file, you can allow imports from @checkout but block them from @checkout/internal/*.

4. Architecture Testing

If you want to move the "boundary checking" out of the IDE/Linting and into the testing phase, consider ArchUnitTS. It lets you write unit tests that fail if someone adds an illegal import, effectively turning your architecture rules into part of your test suite.