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.
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.
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.