Implementing Clean Architecture with TypeScript

RMAG news

Clean Architecture is a software design philosophy that aims to create systems that are easy to maintain, test, and understand. It emphasizes the separation of concerns, making sure that each part of the system has a single responsibility. In this article, we’ll explore how to implement Clean Architecture using TypeScript.

Table of Contents

Introduction to Clean Architecture
Core Principles
Setting Up the Project
Folder Structure
Entities
Use Cases
Interfaces
Frameworks and Drivers
Putting It All Together
Conclusion

Introduction to Clean Architecture

Clean Architecture, introduced by Robert C. Martin (Uncle Bob), provides a clear separation between the different parts of a software system. The main idea is to keep the core business logic independent of external factors such as databases, UI, or frameworks.

Core Principles

Independence: The business logic should be independent of UI, database, or external systems.

Testability: The system should be easy to test.

Separation of Concerns: Different parts of the system should have distinct responsibilities.

Maintainability: The system should be easy to maintain and evolve.

Setting Up the Project

First, let’s set up a TypeScript project. You can use npm or yarn to initialize a new project.

mkdir clean-architecture-ts
cd clean-architecture-ts
npm init -y
npm install typescript ts-node @types/node –save-dev

Create a tsconfig.json file to configure TypeScript.

{
“compilerOptions”: {
“target”: “ES6”,
“module”: “commonjs”,
“outDir”: “./dist”,
“rootDir”: “./src”,
“strict”: true,
“esModuleInterop”: true
}
}

Folder Structure

A clean architecture project typically has the following folder structure:

src/
├── entities/
├── usecases/
├── interfaces/
├── frameworks/
└── main.ts

Entities

Entities represent the core business logic. They are the most important part of the system and should be independent of external factors.

// src/entities/user.entity.ts
export class User {
constructor(id: string, public email: string, public password:string) {}

static create(email: string, password: string) {
const userId = uuid()
return new User(userId, email, password)
}
}

Use Cases

Use cases contain the application-specific business rules. They orchestrate the interaction between entities and interfaces.

// src/usecases/create-user.usecase.ts
import { User } from ../entities/user.entity;
import { UsersRepository } from ../interfaces/users.repository

interface CreateUserRequest {
email: string;
password: string;
}

export class CreateUserUseCase {
constructor(private userRepository: UserRepository) {}

async execute(request: CreateUserRequest): Promise<void> {
const user = User.create(request.email, request.password)
await this.userRepository.save(user);
}
}

Interfaces

Interfaces are the contracts between the use cases and the outside world. They can include repositories, services, or any external system.

// src/interfaces/users.repository.ts
import { User } from ../entities/user.entity;

export interface UserRepository {
save(user: User): Promise<void>;
}

Frameworks and Drivers

Frameworks and drivers contain the implementation details of the interfaces. They interact with external systems like databases or APIs.

// src/frameworks/in-memory-users.repository.ts
import { User } from ../entities/User;
import { UserRepository } from ../interfaces/users.repository;

export class InMemoryUsersRepository implements UserRepository {
private users: User[] = [];

async save(user: User): Promise<void> {
this.users.push(user);
}
}

Putting It All Together

Finally, let’s create an entry point to wire everything together.

// src/main.ts
import { CreateUser } from ./usecases/create-user.usecase;
import { InMemoryUserRepository } from ./frameworks/in-memory-users.repository;

const userRepository = new InMemoryUserRepository();
const createUser = new CreateUserUseCase(userRepository);

createUser.execute({ email: john.doe@example.com, password: 123456 })
.then(() => console.log(User created successfully))
.catch(err => console.error(Failed to create user, err));

Compile and run the project:

tsc
node dist/main.js

Conclusion

By following the principles of Clean Architecture, we can create a system that is maintainable, testable, and adaptable to change. TypeScript provides strong typing and modern JavaScript features that help enforce these principles. With a clear separation of concerns, our codebase becomes easier to understand and evolve over time.

Please follow and like us:
Pin Share