DEV Community

Davi Orlandi
Davi Orlandi

Posted on

Implementing Clean Architecture with TypeScript

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
  2. Core Principles
  3. Setting Up the Project
  4. Folder Structure
  5. Entities
  6. Use Cases
  7. Interfaces
  8. Frameworks and Drivers
  9. Putting It All Together
  10. 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.

mkdir clean-architecture-ts
cd clean-architecture-ts
npm init -y
npm install typescript ts-node @types/node --save-dev
Enter fullscreen mode Exit fullscreen mode

Create a tsconfig.json file to configure TypeScript.

{
  "compilerOptions": {
    "target": "ES6",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true
  }
}
Enter fullscreen mode Exit fullscreen mode

Folder Structure

A clean architecture project typically has the following folder structure:

src/
├── entities/
├── usecases/
├── interfaces/
├── frameworks/
└── main.ts
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

Compile and run the project:

tsc
node dist/main.js
Enter fullscreen mode Exit fullscreen mode

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)