Clean Architecture aims for systems that are easy to maintain, test, and understand. It separates concerns so each part of the system has a single responsibility. Here's how to implement it with 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), keeps core business logic independent of external factors — databases, UI, frameworks.
Core Principles
- Independence: Business logic independent of UI, database, or external systems.
- Testability: Easy to test.
- Separation of Concerns: Distinct responsibilities for different parts of the system.
- Maintainability: Easy to maintain and evolve.
Setting Up the Project
Initialize a TypeScript project with npm or yarn:
mkdir clean-architecture-ts
cd clean-architecture-ts
npm init -y
npm install typescript ts-node @types/node --save-dev
Create a tsconfig.json:
{
"compilerOptions": {
"target": "ES6",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true
}
}
Folder Structure
A typical layout:
src/
├── entities/
├── usecases/
├── interfaces/
├── frameworks/
└── main.ts
Entities
Entities hold core business logic. They 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 application-specific business rules. They orchestrate 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 contracts between use cases and the outside world — repositories, services, external systems.
// 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 implement those interfaces and talk to 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
Wire everything at the entry point:
// 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:
tsc
node dist/main.js
Conclusion
Clean Architecture keeps the system maintainable, testable, and adaptable by isolating business rules from infrastructure. TypeScript's typing helps enforce those boundaries. With clear separation of concerns, the codebase stays easier to understand and change.
Top comments (0)