← Back to blog

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.

Before: relative path hell

import { AuthService } from '../../../core/services/auth.service';

import { UserModel } from '../../../shared/models/user.model';

import { formatDate } from '../../../shared/utils/format-date';
After: path aliases

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.

Watch out: If you ever see 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.

Don't do this

@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.
Do this instead

Import from specific sub-barrels:
@shared/models
@shared/components
@shared/pipes

Result: 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: