Development

Angular Best Practices and Core Concepts: Modules, RxJS and Performance

Our Angular team's practices for enterprise apps: naming, folder structure, modules, lazy loading, trackBy, RxJS and unsubscribing, with notes on what changed.

LinkedIn X

Key takeaways

  • Keep one component, service or directive per file, with kebab-case file names, and let Prettier handle formatting. Updated 2026 Angular's current style guide drops the .component / Component suffixes for new code.
  • Organize the app into root, feature, shared and core areas, use the Angular CLI, and lazy-load features to shrink the initial download. Updated 2026 New apps use standalone components instead of NgModules.
  • Track list items by a unique id (trackBy, or track in the newer @for block), and avoid calling functions in templates; use pre-calculated properties, pure pipes or signals instead.
  • Unsubscribe from long-lived observables (the async pipe, take(1), takeUntil, or takeUntilDestroyed()); one-shot HTTP calls complete on their own. Keep the components inside a screen presentational, with inputs and outputs.
  • Updated 2026 For what's new since this post (standalone components, signals, the new control flow, zoneless change detection), see Angular Best Practices for 2026.

Updated 2026 We first published this post in September 2022, when Angular apps were built with NgModules. Most of the advice still holds, and many enterprise apps still run on that structure. We've fixed the code samples and marked what has changed. For the practices we use on new Angular apps today, see Angular Best Practices for 2026.

Here our Angular team has compiled the best practices they observed while working with Angular for the last 4 years (as of 2022). Some of these observations and practices also appear in other blog posts, which are listed at the end.

  1. Single Responsibility Principle
  2. Angular Coding Style Guide
  3. Naming Conventions and Folder Structure
    • File Naming
    • Class Naming
    • Folder Structure
      • Root Module
      • Feature Module
      • Shared Module
      • Core Module
    • Paths in tsconfig
  4. Make use of Angular CLI
  5. Proper utilization of Lazy Loading
  6. Utilizing trackBy alongside ngFor
  7. Index.ts Usage
  8. Smart Components vs Presentational Components
  9. Properly unsubscribe from observables
    • Use of async pipe
    • Using take(1)
    • Using takeUntil
  10. Avoid using functions in template rendering
  11. Dealing with ExpressionChangedAfterItHasBeenChecked Error
  12. RxJS and Reactive Programming
  13. Frequently asked questions

1. Single Responsibility Principle

Each file should only have one component, directive, service, etc. defined inside of it and should be named as such, e.g. home.component.ts, home.service.ts.

2. Angular Coding Style Guide

  • Rather than having a lengthy and meticulous list of formatting guidelines, just use Prettier to handle formatting, spacing, and module sorting, preferably with a pre-commit hook to ensure this happens automatically before committing code to source control.
  • If the variables have constant values, declare them with const, otherwise declare with let. Avoid using var.
  • Don't start interface names with an uppercase I as you would in some programming languages.

3. Naming Conventions and Folder Structure

3.1 File Naming

Files should use kebab-case and should include their purpose or type, e.g. home-routing.module.ts, home.component.ts, home.component.scss, home.module.ts.

Updated 2026 Angular's style guide was rewritten in 2025 (Angular 20). For new code it drops the type suffix for components, directives and services: home.ts with class Home, and the CLI generates files that way by default. If your project already uses suffixes, stay consistent within the project; mixing the two styles is worse than either one.

3.2 Class Naming

When naming your classes, use the TitleCase style with the added suffix representing your file type, e.g. HomeComponent, AuthService.

Everything else, such as methods and variables, should use camelCase. Private methods and variables can be prefixed with an underscore, e.g. _privateVariable.

3.3 Folder Structure

|-- app
    app.component.ts
    app-routing.module.ts
    app.module.ts
    |-- pages
        |-- home
            |-- [+] components
            |-- home.component.ts
            |-- home-routing.module.ts
            |-- home.module.ts
    |-- core
        |-- [+] authentication
        |-- [+] guards
        |-- [+] interceptors
        |-- [+] services
        |-- core.module.ts
        |-- ensureModuleLoadedOnceGuard.ts
    |
    |-- shared
        |-- [+] components
        |-- [+] directives
        |-- [+] pipes
        |-- [+] models
        shared.module.ts
    |
    |-- [+] configs
|-- assets
    |-- scss
        |-- [+] partials
        |-- _base.scss
        |-- styles.scss

One of the most important things to know about Angular is that it utilizes Angular Modules to tightly group related features. Using Angular modules is an Angular folder structure best practice. Each module should have its folder, and it should be named according to its Module Name. Angular does not distinguish between the different Modules, but we should classify our modules for better understanding.

Updated 2026 Since Angular 19, components are standalone by default and new apps don't need NgModules at all. The same folder structure still works without the .module.ts files: group by feature (pages/home), with core and shared folders for app-wide services and reusable pieces. NgModule apps are still fully supported; see our 2026 post for how we migrate them.

3.3.1 Root Module

Root module is needed for starting an Angular app. This module loads all the root components and other modules. Root Modules are alternatively known as AppModule and are created under the /src/app folder.

3.3.2 Feature Module

Feature module, as the name suggests, implements a specific feature of your Angular app. All the associated pipes, components, and directives specific to that feature are included in its module. Typically discrete screens can be thought of as individual features, but other logical groupings are valid as well.

3.3.3 Shared Module

Next on the list is the shared module. Creating shared modules in your Angular project helps you to organize and streamline your code. You can gather all the commonly used components, pipes, and directives into one module and then import the entire module whenever needed in any part of your application.

3.3.4 Core Module

Core module is a module created to define the core services of your app. The core module has somewhat diminished in usefulness since it has become standard to include @Injectable({providedIn: 'root'}) at the top of every service, which automatically provides each service in root as a singleton. However, it still makes sense for some import once classes to live here, like interceptors, but everything in here could just as easily be grouped into the shared folder section, just make sure to only provide your interceptors in the AppModule.

3.4 Paths in tsconfig

Another way to help with organizing your project and simplifying import paths is making use of the paths section in tsconfig. Here is an example tsconfig with some sections omitted:

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "paths": {
      "@shared/*": ["src/app/shared/*"],
      "@core/*": ["src/app/core/*"],
      "@ui": ["../../libs/ui/src/index.ts"]
    }
  }
}

Now when referencing the shared and core folders, we can use some small absolute paths. This is especially useful when referencing shared libraries in an Nx monorepo:

Without:

import {LessonsService} from "../../core/services/lessons.service";
import {Lesson} from "../../shared/model/lesson";
import {UiModule} from "../../../../../../libs/ui/src/index.ts";

With:

import {LessonsService} from "@core/services/lessons.service";
import {Lesson} from "@shared/model/lesson";
import {UiModule} from "@ui";

4. Make use of Angular CLI

# Install Angular CLI
npm install -g @angular/cli
# Check Angular CLI version
ng version

Angular CLI is a command-line interface tool used for initializing, developing, scaffolding, maintaining, testing, and debugging Angular apps. You should use Angular CLI to build an initial-level structure for your overall application.

Here are some of the essential commands to use with Angular CLI:

  1. ng new create an app that already works, out of the box.
  2. ng generate used for generating components, services, routes, and pipes with test shells.
  3. ng serve helps to test your app locally when developing.
  4. ng test for running various Angular tests on your app.
  5. ng add @angular/pwa helps in setting up the Angular service worker.

As a more extensible alternative, check out Nx.

Updated 2026 The CLI now builds with esbuild and Vite (the application builder), which is much faster than the webpack builder of 2022. ng update also runs migrations for you, including ones that convert apps to standalone components and the new control flow.

5. Proper utilization of Lazy Loading

Lazy loading can drastically reduce the file size of your initially served application by delaying the loading of modules until they're specifically requested. This will help with website loading times, which is important for SEO.

Routing without lazy loading:

const routes: Routes = [
  { 
    path: 'dashboard', 
    component: DashboardComponent
  },
];

Routing with lazy loading, module-based (Angular 14):

const routes: Routes = [
  { 
    path: 'dashboard', 
    loadChildren: () => import(`./dashboard/dashboard.module`).then(
      module => module.DashboardModule
    )
  },
];

This requires the DashboardComponent to be a part of a module DashboardModule with its own DashboardRoutingModule in order to work.

Updated 2026 With standalone components, there's no module to load. Load a single component, or a feature's routes file:

const routes: Routes = [
  {
    path: 'dashboard',
    loadComponent: () => import('./dashboard/dashboard').then(m => m.Dashboard)
  },
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.routes').then(m => m.ADMIN_ROUTES)
  },
];

Inside a template, the @defer block can also lazy-load part of a page, such as a chart below the fold. We cover it in our 2026 post.

6. Utilizing trackBy alongside ngFor

ngFor renders a set of components for each item in an array. Without trackBy, Angular tracks items by object identity, so when the list is replaced with new objects (for example, after reloading it from an API), it re-creates every element. trackBy tells Angular how to identify each item, so only the items that actually changed are re-rendered, improving performance.

Without trackBy

<ul>
  <li *ngFor="let item of collection;">{{item.id}}</li>
</ul>

With trackBy

@Component({
  selector: 'my-app',
  template: `
    <ul>
      <li *ngFor="let item of collection; trackBy: trackById">{{ item.id }}</li>
    </ul>
    <button (click)="getMoreItems()">Add Items</button>
  `,
})
export class AppComponent {
  currentId = 3;
  collection = [{ id: 1 }, { id: 2 }, { id: 3 }];

  getMoreItems() {
    this.currentId++;
    this.collection = [...this.collection, { id: this.currentId }];
  }

  trackById(index: number, item: { id: number }) {
    return item.id;
  }
}

Return a unique id, not the index: tracking by index re-renders items whenever the list is sorted, filtered or has an item inserted.

Updated 2026 The built-in @for block (Angular 17+) replaces *ngFor, and track is required, so you can't forget it:

@for (item of collection; track item.id) {
  <li>{{ item.id }}</li>
}

7. Index.ts Usage

Index.ts can be a useful tool for bundling related classes together. I find this most useful when building libraries or when using a feature-based organization structure.

For example, we have /heroes/index.ts as

//heroes/index.ts

export * from './hero.model';
export * from './hero.service';
export { HeroComponent } from './hero.component';

We can import everything by using source folder name

import { Hero, HeroService } from '@shared/heroes'; // index is implied

8. Smart Components vs Presentational Components

Reference

I tend to think about the categories of where business logic can go in an Angular app in this way: - Application-level or screen-level components - Components used within a screen (often shared) - Services

In general, it is often best to keep components within a screen as pure presentational components, allowing the containing component to handle the logic and behavior via inputs/outputs. Business logic that is used between multiple application-level components should go, and often needs to go in a service. Business logic that is specific to one screen is ok to put directly in the screen component.

An example presentational component:

import {Component, OnInit, Input, EventEmitter, Output} from '@angular/core';
import {Lesson} from "../shared/model/lesson";

@Component({
  selector: 'lessons-list',
  template: `
      <table class="table lessons-list card card-strong">
          <tbody>
          <tr *ngFor="let lesson of lessons" (click)="selectLesson(lesson)">
              <td class="lesson-title"> {{lesson.description}} </td>
              <td class="duration">
                  <i class="md-icon duration-icon">access_time</i>
                  <span>{{lesson.duration}}</span>
              </td>
          </tr>
          </tbody>
      </table>  
  `,
  styleUrls: ['./lessons-list.component.css']
})
export class LessonsListComponent {

  @Input()
  lessons: Lesson[];

  @Output('lesson')
  lessonEmitter = new EventEmitter<Lesson>();

    selectLesson(lesson:Lesson) {
        this.lessonEmitter.emit(lesson);
    }
}

Now let's take a closer look at this component: it does not have the lessons service injected into it via its constructor. Instead, it receives the lessons in an input property via @Input.

This means that the component itself does not know where the lessons come from:

  • the lessons might be a list of all lessons available
  • or the lessons might be a list of all the lessons of a given course
  • or even the lessons might be a page in any given list of a search

We could reuse this component in all of these scenarios, because the lessons list component does not know where the data comes from. The responsibility of the component is purely to present the data to the user and not to fetch it from a particular location.

Here is an example smart component that makes use of the presentation component above:

import { Component, OnInit } from '@angular/core';
import {LessonsService} from "../shared/model/lessons.service";
import {Lesson} from "../shared/model/lesson";

@Component({
  selector: 'app-home',
  template: `
      <h2>All Lessons</h2>
      <h4>Total Lessons: {{lessons?.length}}</h4>
      <div class="lessons-list-container v-h-center-block-parent">
          <lessons-list [lessons]="lessons" (lesson)="selectLesson($event)"></lessons-list>
      </div>
`,
  styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {

    lessons: Lesson[];

  constructor(private lessonsService: LessonsService) {
  }

  ngOnInit() {
    // subscribe to the lessonsService to retrieve the lessons[]
    ...
  }

  selectLesson(lesson) {
    // do some action
    ...
  }
}

Updated 2026 The pattern still holds. In new code, the presentational component declares its inputs and outputs with input() and output() instead of the @Input() and @Output() decorators; see our 2026 post.

9. Properly unsubscribe from observables

Observables need to be unsubscribed from when its containing component is destroyed, otherwise the subscription will hang around in the background causing all sorts of unexpected problems. There are a couple of ways to ensure that observables are unsubscribed from on component destruction:

9.1 Use of async pipe

Binding to observables in the template using the async pipe | async on the template side is generally preferred when possible. This will automatically unsubscribe from an observable when the component is destroyed. This is best used when you can subscribe to an observable directly without using pipe() and can use the data directly in the template without having to apply any logic in the component.

9.2 Using take(1)

take(1) is an operator that will automatically unsubscribe from an observable when one value is received.

data$.pipe(take(1)).subscribe(res=>console.log(res))

9.3 Using takeUntil

takeUntil completes a subscription when another observable emits. This can be used to unsubscribe from all observables when a component is destroyed:

export class MyComponent implements OnInit, OnDestroy {
  private _destroy$ = new Subject<void>();

  constructor(private _messageService: MessageService) {}

  ngOnInit() {
    this._messageService.messages$
      .pipe(takeUntil(this._destroy$))
      .subscribe(message => console.log(message));
  }

  ngOnDestroy() {
    this._destroy$.next();
    this._destroy$.complete();
  }
}

Updated 2026 Since Angular 16, takeUntilDestroyed() does this without the Subject and ngOnDestroy:

export class MyComponent {
  private messageService = inject(MessageService);

  constructor() {
    this.messageService.messages$
      .pipe(takeUntilDestroyed())
      .subscribe(message => console.log(message));
  }
}

10. Avoid using functions in template rendering

Reference

Using functions in template rendering, such as in *ngIf, *ngFor, or in {{interpolation}}, with default change detection (as in, not onPush change detection) can result in horrible performance issues. This is because Angular cannot determine the result of a function without running the function every time; it has no guarantee that the result will be the same given the same set of input parameters. This doesn't sound too bad by itself, but if you have a component somewhere in the tree that has an on mouse move event for example, it will need to run that function every time the mouse is moved.

To solve this, you can switch out functions for pre-calculated property references or pure pipes. Having functions in the template for events (e.g. <button (click)="onClick()">) is still ok.

Bad:

<p>Welcome {{ fullName() }}!</p>
<a href="files" *ngIf="hasAccessTo('files')">Files</a>

Good:

<p>Welcome {{ fullName }}!</p>
<a href="files" *ngIf="'files' | hasAccessTo">Files</a>

This will also require changes in the component logic to pre-calculate the value of fullName, moving the original fullName() function call to wherever the underlying data for fullName() is changed and storing that value in fullName. It will also require moving hasAccessTo() to another file and defining it as a pipe has-access-to.pipe.ts, assuming the function can be written in a way that is pure, as in, it returns the same result with the same inputs every time.

Updated 2026 Signals are the exception. A signal is read by calling it, {{ fullName() }}, and that's cheap: Angular knows when the value changes. A computed() signal is the modern version of the pre-calculated property above. The rule still applies to ordinary methods.

11. Dealing with ExpressionChangedAfterItHasBeenChecked Error

One of the most common and confusing errors in Angular development is the ExpressionChangedAfterItHasBeenChecked error. The error arises when the binding expression changes after angular checked it during the change detection cycle. For more information as to why this happens and how to resolve it, check this Reference

12. RxJS and Reactive Programming

Reference

RxJS is a Reactive Programming library that is used extensively in Angular. It can be difficult to get used to thinking declaratively rather than imperatively, and the topic is rather extensive, but here are some of the basics and the most used concepts in Angular.

Primer Reference

In the most basic of terms, there are streams of data called observables that can be subscribed to and also operators that can alter the form of the streams of data. RxJS provides multiple objects that can be exposed as an observable, the most basic of which is the Subject. Here's an example of what it looks like:

const subject = new Subject<number>();
const observable = subject.asObservable();

ngOnInit() {
    subject.next(5);

    observable.pipe(
        tap((value) => console.log('Initial Value: ', value)),
        map((value) => value * 2)
    ).subscribe((value) => {
        console.log('Post-Map Value: ', value);
    });

    subject.next(1);
    subject.next(2);
    subject.next(3);
}

Output:

Initial Value: 1
Post-Map Value: 2
Initial Value: 2
Post-Map Value: 4
Initial Value: 3
Post-Map Value: 6

What we've done is define a new Subject, exposed it as an observable (this is kind of pointless in this contrived example, usually the Subject would be a private member in a service and the observable would be the way it is exposed publicly. This is to prevent users of the service to providing values to the Subject directly using .next()), applied some operators to the observable using pipe(), subscribed to the stream and push some values to the Subject using .next().

The other most common subject is the BehaviorSubject which functions very similarly to the Subject, but has a starting value when you initialize it, and it will always push its current value immediately when subscribed to. Notice how in the above example, the subject.next(5) does not have its result printed to the console, because it is done before the subscription is made.

There are a lot of operators, all of which you can see here, but the most common ones are:

  • tap - Transparently perform actions or side-effects, such as logging.
  • filter - Emit values that pass the provided condition.
  • take - Emit provided number of values before completing
  • takeUntil - Emit values until provided observable emits.
  • map - Apply projection with each value from source.
  • switchMap - Map to observable, complete previous inner observable, emit values.
  • combineLatest - When any observable emits a value, emit the last emitted value from each.

There are slight variations of map, like switchMap and mergeMap that function slightly differently and some are better for certain situations, as well as variations of combineLatest like withLatestFrom and forkJoin, so be sure to consult the reference to find which one fits your needs if you find yourself trying workarounds to fit the tool to your use case.

Updated 2026 RxJS is still the right tool for streams of events over time: HTTP calls, websockets, search-as-you-type with switchMap and debounceTime. For component state, Angular now has signals, and toSignal() / toObservable() convert between the two. We explain when we use each in our 2026 post.

Frequently asked questions

Should new Angular projects use NgModules or standalone components?

Standalone components. They're the default since Angular 19, and the CLI generates them. NgModules are still supported, so existing apps don't have to migrate all at once; ng generate @angular/core:standalone migrates them step by step.

What is lazy loading in Angular?

Loading part of the app only when the user navigates to it, instead of in the first download. In routes, use loadComponent or loadChildren; inside a template, use @defer.

Why should I use trackBy (or track) in Angular lists?

So Angular can tell which items changed and re-render only those. Track by a unique id, not the index.

How do I unsubscribe from observables in Angular?

Prefer the async pipe in templates. In code, use take(1) for one value, or takeUntilDestroyed() to unsubscribe when the component is destroyed.

Should I use signals or RxJS?

Both. Signals for state that the template reads; RxJS for events and async streams such as HTTP calls and user input. They convert into each other with toSignal() and toObservable().


Need help with an Angular app?

Updated 2026 We build and maintain Angular front ends for business applications, and we upgrade older Angular apps to current versions. See our software development and application modernization services, or talk to an engineer at our Troy, Michigan office.

Sources:

- https://blog.angular-university.io/angular-2-smart-components-vs-presentation-components-whats-the-difference-when-to-use-each-and-why/

- https://medium.com/showpad-engineering/why-you-should-never-use-function-calls-in-angular-template-expressions-e1a50f9c0496

- https://www.learnrxjs.io/

- Updated 2026 https://angular.dev/style-guide

Written by Greg Kabacinski

For SharpQuest, Troy, Michigan.

Join the discussion

No comments yet.

Leave a Reply




Planning a custom software project?

Talk to an engineer at our Troy, Michigan office about what you want to build.

Get new articles by email

One email when we publish. No other mail.

Subscribe to the latest news and updates by email