Skip to content

Clean Architecture Example

clean-architecture/
├── entities/
│ ├── Todo.ts
│ └── User.ts
├── dtos/
│ ├── TodoDTOs.ts
│ └── UserDTOs.ts
├── ports/
│ ├── IInputPort.ts
│ ├── IOutputPort.ts
│ ├── ITodoInputPort.ts
│ └── ITodoOutputPort.ts
├── use-cases/
│ ├── CreateTodoUseCase.ts
│ ├── GetTodoUseCase.ts
│ ├── UpdateTodoUseCase.ts
│ ├── DeleteTodoUseCase.ts
│ ├── GetTodosByUserUseCase.ts
│ ├── CreateUserUseCase.ts
│ ├── GetUserUseCase.ts
│ └── DeleteUserUseCase.ts
├── repositories/
│ ├── ITodoRepository.ts
│ ├── TodoRepository.ts
│ ├── IUserRepository.ts
│ └── UserRepository.ts
├── presenters/
│ ├── CreateTodoPresenter.ts
│ ├── GetTodoPresenter.ts
│ ├── UpdateTodoPresenter.ts
│ ├── DeleteTodoPresenter.ts
│ ├── GetTodosByUserPresenter.ts
│ ├── CreateUserPresenter.ts
│ ├── GetUserPresenter.ts
│ └── DeleteUserPresenter.ts
├── view-models/
│ ├── TodoViewModels.ts
│ └── UserViewModels.ts
├── ioc.config.json
└── container.gen.ts
export class User {
constructor(
public readonly id: string,
public readonly name: string,
public readonly email: string,
private _todos: Todo[] = []
) {}
addTodo(todoData: CreateTodoData): Todo {
const todo = new Todo(
generateId(),
todoData.title,
todoData.description,
todoData.userId
);
this._todos.push(todo);
return todo;
}
get todos(): readonly Todo[] {
return this._todos;
}
}
export class CreateTodoUseCase implements ICreateTodoInputPort {
constructor(
private userRepository: IUserRepository,
private outputPort: ICreateTodoOutputPort
) {}
async execute(todoData: CreateTodoRequestDTO): Promise<void> {
try {
if (!todoData.title.trim()) {
this.outputPort.presentError('Todo title is required');
return;
}
const user = await this.userRepository.findById(todoData.userId);
if (!user) {
this.outputPort.presentError('User not found');
return;
}
const todo = user.addTodo({
title: todoData.title.trim(),
description: todoData.description.trim(),
userId: todoData.userId
});
await this.userRepository.save(user);
this.outputPort.presentSuccess(this.mapToDTO(todo));
} catch (error) {
this.outputPort.presentError(error.message);
}
}
}
export interface ICreateTodoInputPort {
execute(todoData: CreateTodoRequestDTO): Promise<void>;
}
export interface ICreateTodoOutputPort {
presentSuccess(todo: TodoResponseDTO): void;
presentError(error: string): void;
}
export interface CreateTodoRequestDTO {
title: string;
description: string;
userId: string;
}
export interface TodoResponseDTO {
id: string;
title: string;
description: string;
completed: boolean;
userId: string;
createdAt: string;
updatedAt: string;
}
export class CreateTodoPresenter implements ICreateTodoOutputPort {
private result: CreateTodoViewModel | null = null;
presentSuccess(todo: TodoResponseDTO): void {
this.result = {
success: true,
todo: todo,
message: 'Todo created successfully'
};
}
presentError(error: string): void {
this.result = {
success: false,
error: error
};
}
getResult(): CreateTodoViewModel | null {
return this.result;
}
}
{
"source": ".",
"output": "container.gen.ts",
"interface": "I[A-Z].*",
"modules": {
"UserModule": [
"use-cases/*User*",
"repositories/UserRepository.ts",
"presenters/*User*"
],
"TodoModule": [
"use-cases/*Todo*",
"repositories/TodoRepository.ts",
"presenters/*Todo*"
]
}
}
import { container } from './container.gen';
const createUserUseCase = container.userModule.CreateUserUseCase;
const createUserPresenter = container.userModule.CreateUserPresenter;
await createUserUseCase.execute({
name: 'John Doe',
email: 'john@example.com'
});
const userResult = createUserPresenter.getResult();
if (userResult?.success) {
const createTodoUseCase = container.todoModule.CreateTodoUseCase;
const createTodoPresenter = container.todoModule.CreateTodoPresenter;
await createTodoUseCase.execute({
title: 'Learn Clean Architecture',
description: 'Study the principles and patterns',
userId: userResult.user.id
});
const todoResult = createTodoPresenter.getResult();
}