Implementing Clean Architecture with TypeScript
Tags: typescript, javascript, cleancoding, coding
Published: 2024-07-04 19:25:21


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

1. [Introduction to Clean Architecture](#introduction-to-clean-architecture)
2. [Core Principles](#core-principles)
3. [Setting Up the Project](#setting-up-the-project)
4. [Folder Structure](#folder-structure)
5. [Entities](#entities)
6. [Use Cases](#use-cases)
7. [Interfaces](#interfaces)
8. [Frameworks and Drivers](#frameworks-and-drivers)
9. [Putting It All Together](#putting-it-all-together)
10. [Conclusion](#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

1. **Independence**: The business logic should be independent of UI, database, or external systems.
2. **Testability**: The system should be easy to test.
3. **Separation of Concerns**: Different parts of the system should have distinct responsibilities.
4. **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.

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

```json
{
  "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.

```typescript
// 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.

```typescript
// 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.

```typescript
// 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.

```typescript
// 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.

```typescript
// 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:

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