DEV Community

Davi Orlandi
Davi Orlandi

Posted on Edited on

Implementing Clean Architecture with TypeScript

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

  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), keeps core business logic independent of external factors — databases, UI, frameworks.

Core Principles

  1. Independence: Business logic independent of UI, database, or external systems.
  2. Testability: Easy to test.
  3. Separation of Concerns: Distinct responsibilities for different parts of the system.
  4. 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
Enter fullscreen mode Exit fullscreen mode

Create a tsconfig.json:

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

Folder Structure

A typical layout:

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

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

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

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

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

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

Compile and run:

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

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)