Build Scalable Angular Application with Clean Code

Are you an experienced Angular application developer? If yes, your focus should be to build the application and make it scalable, performant, and reliable with neat and clean code. This article will help you write clean code with modular services to build a scalable Angular application.

What is the Clean Code?

Clean code refers to a code which is easy to read, understand, maintain, and extend. It is essential to build a scalable, performant, and reliable application. Clean code is always easy to debug and maintain for the long run.

Use Memoize Pipe in Angular to cache the function results and improve application performance.

Build Scalable Angular Application with Clean Code

I will demonstrate the clean code with a modularized logger service example. I will create separate logger services for the development and production environments and inject them through a dependency injection pattern.

The Scenario

Suppose you are building an enterprise-level Angular application. The application requires various logs to be stored at different places- in the development environment it can be a simple log file but in production, it needs to be AWS CloudWatch or Sentry or something similar.

We will write a generic code to handle logs in both environments efficiently and easily extend to new environments in the future.

Create a Base Logger Service

Define a structure for the base logger service. It will be used as an interface in various environment-specific logger services.

// logger.service.ts

import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export abstract class LoggerService {
  public abstract log(message: string): void;
  public abstract error(message: string): void;
}

The application will interact with a single logger service instead of using various environment-specific logger services or multiple conditions in a single logger service.

Development Logger Service

It is a simple logger service that writes logs to a file in the development environment.

// dev-logger.service.ts

import { Injectable } from '@angular/core';
import { LoggerService } from './logger.service';

@Injectable({ providedIn: 'root' })
export class DevLoggerService extends LoggerService {
  public log(message: string): void {
    console.log(`[DEV LOG]: ${message}`);
    // Code to write log to a file.
  }

  public error(message: string): void {
    console.error(`[DEV ERROR]: ${message}`);
    // Code to write Error log to a file.
  }
}

Production Logger Service

A production-ready logger service that sends logs to AWS CloudWatch or Sentry or something similar.

// prod-logger.service.ts

import { Injectable } from '@angular/core';
import { LoggerService } from './logger.service';

@Injectable({ providedIn: 'root' })
export class ProdLoggerService extends LoggerService {
  log(message: string): void {
    // Send logs to a server (e.g., Sentry)
    console.log(`[PROD LOG]: ${message} - Sent to server`);
  }

  error(message: string): void {
    // Send errors to a server
    console.error(`[PROD ERROR]: ${message} - Sent to server`);
  }
}

Angular Environments

Configure Angular environments if they have not been configured already. Create environment.prod.ts and environment.ts files under src/environments folder.

// environment.prod.ts

export const environment = {
  production: true
};

// environment.ts

export const environment = {
  production: false
};

Make sure angular.json is configured to refer to the correct environment during production build.

// angular.json

{
  "build": {
    "configurations": {
      "production": {
        "fileReplacements": [
          {
            "replace": "src/environments/environment.ts",
            "with": "src/environments/environment.prod.ts"
          }
        ]
      }
    }
  }
}

Include fileReplacements block in your angular.json file if you don’t see it.

Conditional Dependency Injection

Now that you have environment-specific logger services ready, use Angular’s dependency injection to dynamically inject the correct service based on the factory function.

// app.module.ts

import { NgModule } from '@angular/core';

import { LoggerService } from './logger.service';
import { ProdLoggerService } from './prod-logger.service';
import { DevLoggerService } from './dev-logger.service';
import { environment } from '../environments/environment';

@NgModule({
  providers: [
    {
      provide: LoggerService,
      useFactory: () => {
        if (environment.production) {
          return new ProdLoggerService();
        } else {
          return new DevLoggerService();
        }
      },
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Include LoggerService in providers and provide ProdLoggerService and DevLoggerService conditionally through useFactory function. It will inject DevLoggerService in the development environment and ProdLoggerService in production.

Inject Logger in a Component

Now you need to use a single logger service anywhere in the application without any condition in the logger code.

// app.component.ts

import { Component, OnInit } from '@angular/core';
import { LoggerService } from './logger.service';

@Component({
  selector: 'app-root',
  template: `<h1>Welcome to Stackblogger!</h1>`,
})
export class AppComponent implements OnInit {
  constructor(private logger: LoggerService) { }

  ngOnInit(): void {
    this.logger.log('Application initialized');
    this.logger.error('An example error occurred');
  }
}

Logger Testing in Development and Production Environments

Save all the files, run the application and open the application URL in the browser. Open the browser console for the logs.

Logger Service in action in development environment
Logger Service in action in the development environment

We can see the development logger service is running for the local environment.

Let’s test it in the production environment as well. Build the application and use http-server to start the dist folder.

Logger Service in action in the production environment
Logger Service in action in the production environment

The correct logger service is automatically injected for the development and production environments. We can see the DEV and PROD logs respectively.

Why Modular Service is Required

Modularized code has some common benefits. I will list a few of them here.

Clean Code

The component only depends on the LoggerService abstract class. There is no condition in the component or in the logger service to handle development and production environments.

Flexibility

We can easily switch between development and production logger services without code changes.

Scalability

We can easily add more logger services for other environments like staging, QA etc.

Wrap Up

Condition dependency injection in Angular is a powerful feature. It helps developers to build a scalable Angular application with neat and clean code.

If you find this approach helpful, don’t forget to follow me and share this article with other developers.

Leave a Reply

Your email address will not be published. Required fields are marked *