How to Structure Angular Apps in 2021

There are many ways one can structure an Angular app. But this is how I structure my applications for extensive flexibility, scalability, and small initial bundle size.
How to structure angular apps in 2021
Fig-1: Preferred Directory Structure
  • Core: The things that are absolutely essential for the app to start.
  • Features: Business logic. Contains modules, components, services, and other angular building blocks (if required) for a particular business feature.
  • Shared: Components so dumb that it hurts!
  • Pages: Routed components with lazy loaded modules.


Core directory is the place where you put singleton services, injection tokens, constants, app configurations, pipes, interceptors, guards, auth service, utils, etc. that will be used app-wide. If there is something which is specific to the application itself, deployment, CI/CD, API, and the Developer — chances are, it belongs to the core.
Fig1 example core directory
Fig-1: Example Core directory


Business features live in this directory. Make a module per feature. That module can contain components, directives, pipes, services, interfaces, enums, utils, and so on. The idea is to keep things close. So, a pipe, that is solely used in the Speakers module should not be defined in the global scope or inside Core. The same goes for any other angular building block required by this module only.
Fig2 an example feature module
Fig-2: An example feature module
Components are prefixed according to the module name e.g.- if the module name is SpeakersModule, components would be named SpeakerAbcComponent, SpeakerXyzComponent etc. Keep the component tree inside the directory flat. That means, if SpeakerListComponent is the parent and SpeakerListItemComponent is child, do not create speaker-list-item component inside the speaker-list directory. The prefixed naming should be clear to indicate such a relation. The idea is to be able to see what components reside in the module at a glance. Feature modules can import other features and obviously from shared modules.


Consider shared modules a mini library for your UI components. They are not specific to a single business feature. They should be super dumb that you can take all the components, drop in another angular project, and expect to work (given the dependencies are met). You might already know that wrapping UI components provided by other libraries such as Material, ng-zorro-antd, ngx-bootstrap, etc. is a good practice. It protects you from their API changes and allows you to replace the underlying library if required. Components in shared modules are a good place for such wrapping.
Fig3 example shared directory
Fig-3: Example Shared Directory
Do not make a giant SharedModule, rather granularize each atomic feature into its own module (see Fig-3). Criss-cross import of atomic shared modules is allowed, but try to minimize as best as possible. To bring a flavor of a tiny library, you could even prefix the directories & modules with your angular application’s custom prefix (by default it is app ).


Pages directory is the most interesting part of this structure. Think of it like a sink, where feature modules fall into but nothing comes out (i.e- no exported member). In these modules, you do not declare any component other than the page.
Fig4 example page module
Fig-4: Example Page Module
Page controllers have no business logic. They are merely the presenter and orchestrates components from business feature modules. Let’s say — home page. It will contain a header, a hero section, articles, comments, contact, etc. sections — all coming from respective feature modules!
    declarations: HomePageComponent,
    imports: [
export class HomePageModule {}
How a fictional home-page.component.ts might look like:
<main class="container">
They can take help from a page-specific service that combines data and state for that page only. You should provide such service to the page component and NOT in root. Otherwise, the state may persist even after you navigate away from the page because the page component will get destroyed but not the page service.
// home-page.service.ts
export class HomePageService {}

// home-page.component.ts
    providers: HomePageService
export class HomePageComponent {
    constructor(private homePageService: HomePageService){}
The most important purpose of page modules is that each module is loaded lazily to make the app performant and lite.

Every page module is lazy-loaded!

Pro-tip: If you define a single page component per module, then you can claim a further reduction in the initial bundle size. This practice also organizes all routes in a single source (namely AppRoutingModule) which is easier to manage. Then, your app-routing.module.ts file may look like this:
const appRoutes: Routes = [
        path: '',
        loadChildren: () => import('./pages/home-page/home-page.module').then((m) => m.HomePageModule),
        path: 'home',
        redirectTo: '',
        pathMatch: 'full',
        path: 'products/:id',  // <-------- NOTE 1. Child route
        loadChildren: () =>
            import('./pages/product-details-page/product-details-page.module').then((m) => m.ProductDetailsPageModule),
        path: 'products',     // <--------- NOTE 2. Parent route
        loadChildren: () =>
            import('./pages/product-list-page/product-list-page.module').then((m) => m.ProductListPageModule),
        path: 'checkout/pay',
        loadChildren: () =>
            import('./pages/checkout-payment-page/checkout-payment-page.module').then((m) => m.CheckoutPaymentPageModule),
        path: 'checkout',
        loadChildren: () => import('./pages/checkout-page/checkout-page.module').then((m) => m.CheckoutPageModule),
        path: '**',
        loadChildren: () => import('./pages/not-found-page/not-found-page.module').then((m) => m.NotFoundPageModule),
Notes 1 & 2: Since route declarations are parsed top-to-bottom, be sure to declare child paths before the parent path. This will ensure lazy-loading chunks fetched correctly. Otherwise, if you define the parent route first, then visiting any child route will also load the parent route’s module chunk unnecessarily. You can see the difference in DevTools. Here is my experiment when I put parent route first (Fig-5.1) VS child route first (Fig-5.2) and visit
Fig51 when products declared before productsid in routes config
Fig-5.1: When /products declared BEFORE /products/:id in routes config
Fig52 when products declared after productsid in routes config
Fig-5.2: When /products declared AFTER /products/:id in routes config


Add the directory paths to your tsconfig.json file so that import paths in your application are shorter and nicer:
// tsconfig.json
    "compilerOptions": {
        "baseUrl": "./",
        "paths": {
            "@core/*": "src/app/core/*",
            "@features/*": "src/app/features/*",
            "@shared/*": "src/app/shared/*",
            "@environment/*": "src/environments/*"
        "outDir": "./dist/out-tsc",
Now your imports will be aliased likeimport { Nice } from '@features/nice instead of import { Ugly } from './../../path/to/ugly . Thanks for reading!
Popular articles