← Back to list

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…

Zulfikar Ditya · 2026-05-14 03:52 · 5 claps · 4.2 min read
#typescript #nodejs #software-development #software-engineering #proggraming
Open on Medium ↗
Wiki topics: 🌐 · Web Development

Barrel — The Quiet Pattern That Cleans Up Your Elysia Imports

Photo by Daniel Vogel on Unsplash

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:

  1. Circular dependencies. Especially between modules/* and libs/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.
  2. 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 existing index.ts)
  • Never barrel Drizzle schema files
  • Prefer export { x } from './x' over export * — it's explicit and tree-shakes better if you ever bundle
  • Use import type for type-only re-exports
  • Regenerate with bun run barrel before 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