Clean Architecture Example
Project Structure
Section titled “Project Structure”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
Entities
Section titled “Entities”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; }}
Use Cases
Section titled “Use Cases”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;}
Presenters
Section titled “Presenters”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; }}
Configuration
Section titled “Configuration”{ "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();}