Clean Architecture for Angular Applications

Moiz Nadeem
Taager Tech Blog
Published in
9 min readSep 22, 2022

Architecture — Frontend:

Many people seem to roll their heads directly towards the backend side of things once they hear Architecture. A commonly misunderstood concept is that the front-end doesn’t need architecture at all or even if it does, it does not have much impact. We’ll be discussing the entirety of all of this being untrue.

Why Frontend Architecture is required:

Think of any building that has no architecture, everything is left for the builders to decide what to do and how to set up the entire building. When the decision is left to the builders, one of the builders might have expertise in stone walls so they opt for making one side of the building with the stone wall while the second builder might be well versed with woodwork and decides to make the entire wall with woodwork. Now the entire building will be a disaster because of inconsistency, and above all might not be able to hold the structure planned for the building and would eventually collapse.

The same is the case with Web Applications. The entirety of an application is dependent on the system design philosophy. The architect of the system ensures that the end product will be not only achieved but also that the system will still be able to accommodate any amendments that might occur.

In terms of Frontend architecture, an entire project standard is set up in which the code is testable, code consistency is ensured and a final product is envisioned.

One of the sleekest, most adaptive, scalable, and structured architectures out there is Clean Architecture.

Clean Architecture:

Clean Architecture is also related to Domain-Driven Design, It takes several characteristics from DDD. Overall Clean Architecture is a combination of the following architectures, where it sums up the essence of the architecture into one giving a robust architecture to withhold applications on a large scale.

The principle idea with clean architecture is to keep the core business logic and application logic as separate entities so changes in one doesn’t necessarily affect the other. Using this architecture we make domain or entities completely independent of the presentation layer so no changes in the UI can affect the domain or any entity. Using this architecture as a base we get the entire liberty to have a system that is flexible enough to deal with generic changes, UI changes, Interface changes, and even technology changes where the core application stays the same and is completely independent of presentation layers, databases & Infrastructure.

The flow of the dependency with respect to Clean Architecture is always from outside towards inside i.e from the presentation layer towards the domain/business layer.

Clean Architecture in terms of Angular:

In this blog, we’ll be directly jumping on the implementation of Clean Architecture on an Angular Application.

Before we move on towards the implementation it is important to mention that in an ideal case Clean Architecture is not dependent on framework or language. In the same sense, the Domain layer should always be agnostic of the framework we use or any other details.

However, as per our implementation, the domain layer is dependent on the language (Typescript in this particular case) although in case of a framework (Angular) shift we won’t have to touch the domain layer at all.

We’ll discuss in detail about the implementation from the domain layer to the presentation layer and discuss the folder structuring for the entire application.

In this example, we’ll be looking at a simple User application with user login, register & getting the user profile.

Folder structure:

After the creation of the application, we set up the folder structure in a way through which we can separate each of the layers separate.

src/
├─ domain/
├─ data/
├─ presentation/

Domain:

This folder can contain all the business models, use cases, interactors & repository abstractions for the application. Pure relevancy to business logic which will ONLY change if a change in business requirements occurs.

Data:

This folder can contain all the processors of the application, the repository implementations, data source models & mappers.

Presentation:

This folder can contain all the UI for the application. All style files & components that build up to form the entire UI experience. All components will use the entire logic from the data and core in this layer.

Please note the presentation layer implementation is not in the scope of this project nor included in the code.

Implementation:

Let's get starting on the code:

Setting up base files

In the src folder, we create a new folder for the base files.

These base files will be responsible for setting up a template for the use case and mapper which we will use later.

Create a new folder inside src called base and then create a new folder utils followed by a new file called mapper.ts inside src/base/utils.

So the directory would now be:

src/
├─ base/
│ ├─ utils/
│ │ ├─ mapper.ts

In the mapper.ts file create a class that will create mapping To and From

export abstract class Mapper<I, O> {
abstract mapFrom(param: I): O;
abstract mapTo(param: O): I;
}

src/base/utils/mapper.ts

Create a folder called domain inside the src and then create a folder called base in src/domain and then create a file titled use-case.ts All our use cases will have a dependency on this base usecase.ts file.

import { Observable } from 'rxjs';export interface UseCase<S, T> {
execute(params: S): Observable<T>;
}

src/domain/base/use-case.ts

Now create a models folder inside the src/domain folder and add the model for the user which will serve as the primary business logic for the user’s existence. The models folder will contain all the models for the business logic. Create a file user.model.ts inside the newly created domain/models folder. src/domain/models/user.model.ts

In the user model file add all the attributes of the user from the general perspective. This doesn’t have to depend on any API and data you might be managing for the http requests but solely on what the business demands from the user object.

For our example we’ll go with the following:

export interface UserModel {
id: string;
fullName: string;
username: string;
email?: string;
phoneNum: string;
createdAt?: Date;
profilePicture: string;
activationStatus: boolean;
}

src/domain/models/user.model.ts

Next, let's create another folder for handling the repositories so we’ll name it repositories and create a file named user.repository.ts. In this repository, we’ll create an abstract class that will define all the actions performed with the user model. We will implement login, register, and user activation for the account.

import { Observable } from 'rxjs';
import { UserModel } from '../models/user.model';
export abstract class UserRepository {
abstract login(params: {username: string, password: string}): Observable<UserModel>;
abstract register(params: {phoneNum: string, password: string}): Observable<UserModel>;
abstract getUserProfile(): Observable<UserModel>;
}

src/domain/repositories/user.repository.ts

Last but not least we’ll finally add the use cases for our application. Each of the use-cases will have a separate file so it's easy to manage and keep all the actions independent so, later if there’s any change required other use-cases are not disturbed.

Create a folder named use cases inside the domain folder and create files for each of the use case as follows:

import { Observable } from 'rxjs';
import { UseCase } from '../base/use-case';
import { UserModel } from '../models/user.model';
import { UserRepository } from '../repositories/user.repository';
export class UserLoginUseCase implements UseCase<{ username: string; password: string }, UserModel> { constructor(private userRepository: UserRepository) { } execute(
params: { username: string, password: string },
): Observable<UserModel> {
return this.userRepository.login(params);
}
}

src/domain/usecases/user-login.usecase.ts

Notice the name of the file includes the usecase suffix in it so it's clear that the respective file is usecase.

import { Observable } from 'rxjs';
import { UseCase } from '../base/use-case';
import { UserModel } from '../models/user.model';
import { UserRepository } from '../repositories/user.repository';
export class UserRegisterUseCase implements UseCase<{ phoneNum: string; password: string }, UserModel> { constructor(private userRepository: UserRepository) { } execute(
params: { phoneNum: string; password: string },
): Observable<UserModel> {
return this.userRepository.register(params);
}
}

src/domain/usecases/user-register.usecase.ts

import { Observable } from 'rxjs';
import { UseCase } from '../base/use-case';
import { UserModel } from '../models/user.model';
import { UserRepository } from '../repositories/user.repository';
export class GetUserProfileUseCase implements UseCase<void, UserModel> { constructor(private userRepository: UserRepository) { } execute(): Observable<UserModel> {
return this.userRepository.getUserProfile();
}
}

src/domain/usecases/get-user-profile.usecase.ts

With that, we’re pretty much done in the domain folder.

This is how the structure should look so far:

src/
├─ base/
│ ├─ mapper.ts
├─ domain/
│ ├─ base/
│ | ├─ use-case.ts
│ ├─ models/
│ │ ├─ user.model.ts
│ ├─ repositories/
│ │ ├─ user.repository.ts
│ ├─ usecases/
│ │ ├─ user-login.usecase.ts
│ │ ├─ user-register.usecase.ts
│ │ ├─ get-user-profile.usecase.ts

Now towards the data folder.

We’ll create repositories folder inside the data folder. In this folder, we’ll create another folder for the user that will connect us with the API for the use cases that we need to perform.

First, let's create an entity for the User. User Entity will be as per the response that you receive from the Database via the API. It could be the same as your model but in some cases, it can differ from the model.

Creating the entity file in src/data/respositories/user/entities

export interface UserEntity {
id: string;
name: string;
userName: string;
phoneNumber: string;
userPicture: string;
activationStatus: boolean;
}

src/data/repositories/user/entities/user-entity.ts

Now let's create a mapper for the implementation. Mapping will map the properties from UserModel to UserEntity and vice-versa.

import { Mapper } from 'src/base/mapper';
import { UserModel } from 'src/domain/models/user.model';
import { UserEntity } from '../entities/user-entity';
export class UserImplementationRepositoryMapper extends Mapper<UserEntity, UserModel> {
mapFrom(param: UserEntity): UserModel {
return {
id: param.id,
fullName: param.name,
username: param.userName,
phoneNum: param.phoneNumber,
profilePicture: param.userPicture,
activationStatus: param.activationStatus
};
}
mapTo(param: UserModel): UserEntity {
return {
id: param.id,
name: param.fullName,
userName: param.username,
phoneNumber: param.phoneNum,
userPicture: param.profilePicture,
activationStatus: param.activationStatus
}
}
}

src/data/repositories/user/mappers/user-repository.mapper.ts

With all that being set up let's move towards the implementation repository where we execute the actual usecases. This user-implementation repository will execute all the usecases stated for the user.

Creating the user-implementation.repository.ts

import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { UserEntity } from './entities/user-entity';
import { UserImplementationRepositoryMapper } from './mappers/user-repository.mapper';
import { UserRepository } from 'src/domain/repositories/user.repository';
import { UserModel } from 'src/domain/models/user.model';
@Injectable({
providedIn: 'root',
})
export class UserImplementationRepository extends UserRepository {
userMapper = new UserImplementationRepositoryMapper();
constructor(private http: HttpClient) {
super();
}
login(params: {username: string, password: string}): Observable<UserModel> {
return this.http
.post<UserEntity>('https://example.com/login', {params})
.pipe(map(this.userMapper.mapFrom));
}
register(params: {phoneNum: string, password: string}): Observable<UserModel> {
return this.http
.post<UserEntity>('https://example.com/register', {params})
.pipe(map(this.userMapper.mapFrom));
}
getUserProfile(): Observable<UserModel>{
return this.http.get<UserEntity>('https://example.com/user').pipe(
map(this.userMapper.mapFrom));
}
}

src/data/repositories/user/user-implementation.repository.ts

With that, the entire implementation setup its now time to provide all the use cases to the provider of the data module.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import { UserRepository } from 'src/domain/repositories/user.repository';
import { UserLoginUseCase } from 'src/domain/usecases/user-login.usecase';
import { UserRegisterUseCase } from 'src/domain/usecases/user-register.usecase';
import { GetUserProfileUseCase } from 'src/domain/usecases/get-user-profile.usecase';
import { UserImplementationRepository } from './repositories/user/user-implementation.repository';
const userLoginUseCaseFactory =
(userRepo: UserRepository) => new UserLoginUseCase(userRepo);
export const userLoginUseCaseProvider = {
provide: UserLoginUseCase,
useFactory: userLoginUseCaseFactory,
deps: [UserRepository],
};
const userRegisterUseCaseFactory =
(userRepo: UserRepository) => new UserRegisterUseCase(userRepo);
export const userRegisterUseCaseProvider = {
provide: UserRegisterUseCase,
useFactory: userRegisterUseCaseFactory,
deps: [UserRepository],
};
const getUserProfileUseCaseFactory =
(userRepo: UserRepository) => new GetUserProfileUseCase(userRepo);
export const getUserProfileUseCaseProvider = {
provide: GetUserProfileUseCase,
useFactory: getUserProfileUseCaseFactory,
deps: [UserRepository],
};
@NgModule({
providers: [
userLoginUseCaseProvider,
userRegisterUseCaseProvider,
getUserProfileUseCaseProvider,
{ provide: UserRepository, useClass: UserImplementationRepository },
],
imports: [
CommonModule,
HttpClientModule,
],
})
export class DataModule { }

src/data/data.module.ts

With all that being done now all that is left is to execute the usecase that will execute the implementation repository.

For this in the presentation folder create your separate components and in the constructor import the usecases as per the component and use the execute function and then subscribe or call it however you’d call a normal API function.

The final folder structure would be pretty much similar to this:

src/
├─ base/
│ ├─ mapper.ts
├─ domain/
│ ├─ base/
│ | ├─ use-case.ts
│ ├─ models/
│ │ ├─ user.model.ts
│ ├─ repositories/
│ │ ├─ user.repository.ts
│ ├─ usecases/
│ │ ├─ user-login.usecase.ts
│ │ ├─ user-register.usecase.ts
│ │ ├─ get-user-profile.usecase.ts
├─ data/
│ ├─ respositories/
│ │ ├─ user/
│ │ │ ├─ entities/
│ │ │ │ ├─ user-entity.ts
│ │ │ ├─ mappers/
│ │ │ │ ├─ user-repository.mapper.ts
│ │ │ ├─ user-implementation.repository.ts
│ ├─ data.module.ts
├─ presentation/
│ ├─ your_structure_for_components

Code and Github link:

You can find the code here: https://gitlab.com/taager-com/examples/-/tree/main/clean-architecture-angular

References:

https://www.freecodecamp.org/news/a-quick-introduction-to-clean-architecture-990c014448d2/

https://www.c-sharpcorner.com/article/what-is-clean-architecture/

https://bespoyasov.me/blog/clean-architecture-on-frontend/

--

--