Sexy Imports: Path Aliases and Barrel Files in Angular
If you’ve ever typed ../../../shared/utils/format-date and felt your soul leave your body, this post is for you. Relative imports in any non-trivial Angular project become unreadable fast. You move a file, half your imports break. You refactor a folder, you’re fixing dots for twenty minutes.
There’s a better way. TypeScript gives you path aliases. Combine them with barrel files, and your imports go from ugly to clean in an afternoon.
The Problem
Here’s what a typical component looks like in a medium-sized Angular project:
import { AuthService } from '../../../core/services/auth.service';
import { UserModel } from '../../../shared/models/user.model';
import { formatDate } from '../../../shared/utils/format-date';
import { ButtonComponent } from '../../../shared/components/button/button.component';
import { API_CONFIG } from '../../../core/config/api.config';
Five imports. Five mental exercises in “how many directories up do I need to go?” Move this file to a different folder and every single one breaks.
import { AuthService } from '../../../core/services/auth.service';import { UserModel } from '../../../shared/models/user.model';import { formatDate } from '../../../shared/utils/format-date';
import { AuthService } from '@core/services/auth.service';import { UserModel } from '@shared/models/user.model';import { formatDate } from '@shared/utils/format-date';
The second version doesn’t change no matter where your file lives. It’s absolute. It’s readable. It tells you exactly where things come from.
Setting Up Path Aliases in Angular
Angular uses TypeScript’s paths option in tsconfig.json. Open your tsconfig.json (the root one, not tsconfig.app.json) and add your aliases:
{
"compilerOptions": {
"baseUrl": "src",
"paths": {
"@app/*": ["app/*"],
"@core/*": ["app/core/*"],
"@shared/*": ["app/shared/*"],
"@features/*": ["app/features/*"],
"@env/*": ["environments/*"]
}
}
}
That’s it. No extra tooling. No webpack config. No third-party packages. Angular CLI picks up tsconfig.json paths natively. Your IDE will too — VS Code resolves them automatically for autocomplete and go-to-definition.
The baseUrl is set to src, so all the path values are relative to that folder.
Adding Barrel Files
Path aliases clean up the prefix. Barrel files clean up the suffix. A barrel file is just an index.ts that re-exports things from a folder.
Create src/app/shared/models/index.ts:
export { UserModel } from './user.model';
export { ProductModel } from './product.model';
export { OrderModel } from './order.model';
Now instead of importing each model by its full path, you import from the folder:
import { UserModel, ProductModel } from '@shared/models';
TypeScript resolves @shared/models to @shared/models/index.ts automatically. One import line, multiple symbols, clean path.
The Full Setup
Here’s a practical folder structure with barrel files at each level:
src/app/
├── core/
│ ├── services/
│ │ ├── auth.service.ts
│ │ ├── http.service.ts
│ │ └── index.ts
│ ├── guards/
│ │ ├── auth.guard.ts
│ │ └── index.ts
│ └── index.ts
├── shared/
│ ├── models/
│ │ ├── user.model.ts
│ │ └── index.ts
│ ├── components/
│ │ ├── button/
│ │ └── index.ts
│ └── index.ts
└── features/
└── dashboard/
With this in place, your imports become:
import { AuthService, HttpService } from '@core/services';
import { AuthGuard } from '@core/guards';
import { UserModel } from '@shared/models';
import { ButtonComponent } from '@shared/components';
Short. Stable. Scannable.
When Barrel Files Hurt
Barrel files aren’t free. There are two real problems you should know about.
Circular dependencies. If module A exports from module B through a barrel, and module B imports from module A’s barrel, you get a circular dependency. Angular won’t always throw an obvious error — you’ll just get weird undefined values at runtime. This is the number one reason barrel files cause pain.
Tree-shaking. When you import one thing from a barrel that re-exports fifty things, bundlers should tree-shake the rest. Modern Angular with esbuild handles this well. But deep barrel chains — barrels that re-export from other barrels — can confuse the bundler and pull in more code than you need.
undefined where you expected a class or value, check for circular barrel imports first. Run npx madge --circular src/ to detect them automatically.
Rules for Barrel Files That Don’t Bite You
Here’s what I’ve landed on after using barrels across multiple Angular projects:
Keep barrels shallow. One level of re-export. @shared/models/index.ts exports models. @shared/index.ts does not re-export everything from @shared/models. Deep barrel chains cause the circular dependency and tree-shaking issues.
Don’t barrel everything. Feature modules usually don’t need barrels. Only create them for shared/ and core/ — code that gets imported across the app. If a module is only used internally, skip the barrel.
Export the public API only. A barrel should expose what other modules need. Internal helpers, private types, implementation details — leave them out. Think of the barrel as the module’s public interface.
One barrel per folder. Don’t create a single mega-barrel at @shared that re-exports hundreds of symbols. Keep barrels close to the code they export. Import from the most specific path that makes sense.
@shared/index.ts re-exports from ./models, ./components, ./pipes, ./utils, ./directives — hundreds of exports in one barrel.Result: circular deps, slow IDE, poor tree-shaking.
Import from specific sub-barrels:
@shared/models@shared/components@shared/pipesResult: fast resolution, no surprises.
A Note on Angular Libraries
If you’re using Nx or Angular workspace libraries, path aliases are even more natural. Each library gets its own alias in the root tsconfig.base.json, and you import across libraries with clean paths like @myorg/shared-ui or @myorg/data-access. The same principles apply — just at a larger scale.
The Takeaway
Set up path aliases in your tsconfig.json. It takes five minutes. Add barrel files to your shared/ and core/ folders. Keep them shallow. Don’t re-export the world.
Your imports will be shorter, more stable, and actually tell you where things come from. You’ll stop counting dots. Your diffs will be cleaner because moving files won’t cascade import changes across the project.
It’s a small change to your project config. It’s a big change to how your codebase reads.
Sources:
- TypeScript Path Mapping — official TypeScript documentation on paths configuration
- madge — tool for detecting circular dependencies