Barrel — The Quiet Pattern That Cleans Up Your Elysia Imports
You open src/modules/auth/index.ts and the imports stretch past your screen. Repository here, service there, validator two folders up…
Barrel — The Quiet Pattern That Cleans Up Your Elysia Imports
Photo by Daniel Vogel on Unsplash
You open src/modules/auth/index.ts and the imports stretch past your screen. Repository here, service there, validator two folders up, types from somewhere else, errors from libs. You scroll past it every time.
There is a small pattern that fixes this. It has been sitting in the Angular style guide since 2016, and it pairs beautifully with how Elysia projects are already structured.
It is called a barrel, and once you understand where it belongs (and where it absolutely does not), your Bun + Elysia codebase gets noticeably quieter.
What a Barrel Actually Is
A barrel is an index.ts file that re-exports things from its neighbors. Nothing more.
Before:
import { authService } from './modules/auth/auth.service';
import { authRepository } from './modules/auth/auth.repository';
import { LoginSchema } from './modules/auth/auth.validator';
After, with a barrel at ./modules/auth/index.ts:
import { authService, authRepository, LoginSchema } from './modules/auth';
The term comes from the Angular team — the idea of “rolling up” exports into one container. The pattern itself is older than that, but the name stuck.
The Clean Elysia Layout
To keep this concrete, I’ll use aolus-software/clean-elysia as the reference. It’s an Elysia + Bun + Drizzle boilerplate following clean architecture, and its folder layout is exactly where barrels earn their keep:
src/
├── base.ts
├── index.ts
├── libs/
│ ├── database/
│ ├── plugins/
│ ├── repositories/
│ └── utils/
└── modules/
├── auth/
├── profile/
└── settings/
Two layers, two different barrel strategies. The libs/ folder benefits the most. The modules/ folder needs a lighter touch.
Barrels at the Module Boundary
Inside a typical module:
auth/
├── index.ts ← Elysia controller (also the public face)
├── service.ts
├── sc.ts
Here’s the nice part: in Elysia, a module’s index.ts is already the public face. It exports the Elysia instance that base.ts mounts via .use(). You don't need to invent a barrel — the convention gives you one.
// modules/auth/index.ts
import { Elysia } from 'elysia';
import { authService } from './service';
import { LoginSchema } from './schema';
export const auth = new Elysia({ prefix: '/auth' })
.post('/login', ({ body }) => authService.login(body), {
body: LoginSchema,
});
If you ever want to expose internal pieces (say, the service for testing), add a small barrel.ts alongside index.ts rather than overloading the controller file.
Barrels in libs/ — Where They Actually Shine
This is where I’d reach for barrels every time.
// libs/plugins/index.ts
export { loggerPlugin } from './logger.plugin';
export { errorPlugin } from './error.plugin';
export { securityPlugin } from './security.plugin';
export { rateLimitPlugin } from './rate-limit.plugin';
Now base.ts reads cleanly:
// Before
import { loggerPlugin } from './libs/plugins/logger.plugin';
import { errorPlugin } from './libs/plugins/error.plugin';
import { securityPlugin } from './libs/plugins/security.plugin';
import { rateLimitPlugin } from './libs/plugins/rate-limit.plugin';
// After
import {
loggerPlugin,
errorPlugin,
securityPlugin,
rateLimitPlugin,
} from './libs/plugins';
Same treatment for libs/repositories, libs/utils, and libs/errors. These folders rarely change shape, and consumers don't care about internal file layout. That's the textbook barrel use case.
The package.json Scripts
Clean Elysia already uses Bun scripts heavily (bun run dev, bun run db:generate). Add a barrel script that fits the same pattern:
{
"scripts": {
"dev": "bun run --hot src/index.ts",
"build": "bun build src/index.ts --outdir ./dist --target bun",
"start": "bun run dist/index.js",
"barrel": "barrelsby --delete --directory ./src/libs --directory ./src/modules",
"barrel:check": "barrelsby --directory ./src/libs --directory ./src/modules --noSemicolon --location top"
}
}
Install once:
bun add -d barrelsby
Now you regenerate every barrel in libs/ and modules/ with one command:
bun run barrel
I add this to a pre-commit hook (Clean Elysia already uses Husky) so the team never hand-edits an index.ts that should be generated.
The VSCode Extension Option
If running a CLI feels heavy for one folder, there’s a faster path: TypeScript Barrel Generator by Elio Struyf (eliostruyf.vscode-typescript-exportallmodules).
Right-click any folder in the explorer → Generate Barrel File. It writes the index.ts for you, picks up named exports automatically, and lets you configure recursion and file extensions in settings.
Two alternatives if it doesn’t fit your workflow:
- Auto Barrel (
imgildev.vscode-auto-barrel) — watches folders and regenerates automatically - Barreler (
shinruchan.vscode-barreler) — minimal one-click generator
My rule: use the extension for one-off edits while building a feature, and bun run barrel for full refactors or CI.
When Barrels Hurt
Here’s the honest part. Barrels have killed real codebases.
Atlassian removed barrels from a large internal codebase and saw TypeScript highlighting improve 30%, local tests run 50% faster, and CI test execution drop by 88%. TkDodo cut a Next.js page’s module count from 11,000 to 3,500 — a 68% reduction — by removing internal barrels.
The mechanism is simple: when you import one thing from a barrel, the runtime (or TypeScript, or your test runner) has to resolve and load every other thing the barrel exports. In a frontend bundle, that cascades.
For a Bun-based Elysia API the impact is smaller — there’s no bundler shipping to a browser — but two real risks remain:
- Circular dependencies. Especially between
modules/*andlibs/repositories. A module imports from a repository barrel, and the barrel transitively re-exports something that imports back. Elysia's.use()chains can hide these for a while before they bite. - Drizzle schema files. Do not barrel
libs/database/schema/*. Drizzle's type inference relies on direct imports of schema objects, and a barrel breaks it in subtle ways.
Practical Rules for This Stack
- Barrel
libs/*aggressively — those folders are stable - Barrel
modules/*only at the controller boundary (the existingindex.ts) - Never barrel Drizzle schema files
- Prefer
export { x } from './x'overexport *— it's explicit and tree-shakes better if you ever bundle - Use
import typefor type-only re-exports - Regenerate with
bun run barrelbefore commits, not by hand
The Quiet Win
Go back to that auth/index.ts you opened at the start. The screen-spanning imports? With barrels at libs/plugins, libs/repositories, and libs/utils, that block collapses to three or four lines. The file becomes about the route, not about plumbing.
Barrels are a tool, not a religion. Clean architecture already pushes you toward the right module boundaries — barrels just make those boundaries feel like real APIs instead of folder paths.
Generate them. Don’t hand-write them. Skip the folders where they hurt. Then forget they exist.
That last part is the whole point.
메타데이터
- post_id
- 5db25f0b59a2
- slug
- barrel-the-quiet-pattern-that-cleans-up-your-elysia-imports-5db25f0b59a2
- url
- https://medium.com/@zulfikarditya/barrel-the-quiet-pattern-that-cleans-up-your-elysia-imports-5db25f0b59a2
- canonical_url
- https://medium.com/@zulfikarditya/barrel-the-quiet-pattern-that-cleans-up-your-elysia-imports-5db25f0b59a2
- author_url
- https://medium.com/@zulfikarditya
- status
- ok
- fetched_at
- 2026-07-18 06:23:57