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.
Top comments (0)