HOW TO STRUCTURE ANGULAR APPS IN 2021
Mar 30 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

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

Features

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.

Shared

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

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!
@NgModule({
    declarations: HomePageComponent,
    imports: [
        CommonModule,
        ArticlesModule,
        CommentsModule,
        ContactModule,
        HeadersModule,
        HomePageRoutingModule,
    ],
})
export class HomePageModule {}
How a fictional home-page.component.ts might look like:
<app-header-default></app-header-default>
<main class="container">
    <app-hero-content></app-hero-content>
    <app-article-list></app-article-list>
    <app-comment-list-latest></app-comment-list-latest>
    <app-contact-form></app-contact-form>
</main>
<app-footer-default></app-footer-default>
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
@Injectable()
export class HomePageService {}

// home-page.component.ts
@Component({
    ...
    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 http://mysite.com/products/1.
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

Bonus

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!
BLOG LATEST
Popular articles
GOT AN IDEA? LET'S DISCUSS!
Share your project’s scope, timeline, technical requirements, business challenges, and other details you consider necessary. Our team will study them and contact you soon. Let’s make an exciting product together!
By sending this form I confirm that I have read and accept the Privacy Policy
WELCOME TO OUR OFFICES
Georgia
Russia