Minimal Todo Example
This example demonstrates the simplest possible IoC Arise setup with a todo list application, showing how to use interface-based dependency injection with repository and service patterns in just a few files.
Project Structure
Section titled “Project Structure”Directoryminimal-todo/
Directoryentities/
- Todo.ts Todo entity class with business logic
Directoryrepositories/
- ITodoRepository.ts Repository interface
- InMemoryTodoRepository.ts In-memory repository implementation
Directoryservices/
- ITodoService.ts Service interface
- TodoService.ts Service implementation
- container.gen.ts Generated IoC container
- ioc.config.json IoC configuration
- README.md
Todo Entity
Section titled “Todo Entity”The Todo entity includes built-in business logic and validation:
export interface CreateTodoData { title: string; description?: string;}
export interface UpdateTodoData { title?: string; description?: string; completed?: boolean;}
export class Todo { id: string; title: string; description?: string; completed: boolean; createdAt: Date; updatedAt: Date;
constructor(data: CreateTodoData) { this.id = Math.random().toString(36).substr(2, 9); this.title = data.title; this.description = data.description; this.completed = false; this.createdAt = new Date(); this.updatedAt = new Date(); }
markAsCompleted(): void { this.completed = true; this.updatedAt = new Date(); }
markAsPending(): void { this.completed = false; this.updatedAt = new Date(); }
update(data: UpdateTodoData): void { if (data.title !== undefined) this.title = data.title; if (data.description !== undefined) this.description = data.description; if (data.completed !== undefined) this.completed = data.completed; this.updatedAt = new Date(); }}
Repository Pattern
Section titled “Repository Pattern”How do we abstract data access? Through interfaces and implementations:
export interface ITodoRepository { create(todo: Todo): Promise<Todo>; findById(id: string): Promise<Todo | undefined>; findAll(): Promise<Todo[]>; update(id: string, data: UpdateTodoData): Promise<Todo | undefined>; delete(id: string): Promise<boolean>; findByCompleted(completed: boolean): Promise<Todo[]>;}
export class InMemoryTodoRepository implements ITodoRepository { private todos: Todo[] = [];
async create(todo: Todo): Promise<Todo> { this.todos.push(todo); return todo; }
async findById(id: string): Promise<Todo | undefined> { return this.todos.find(todo => todo.id === id); }
async findAll(): Promise<Todo[]> { return [...this.todos]; }
async update(id: string, data: UpdateTodoData): Promise<Todo | undefined> { const todo = this.todos.find(t => t.id === id); if (todo) { todo.update(data); return todo; } return undefined; }
async delete(id: string): Promise<boolean> { const initialLength = this.todos.length; this.todos = this.todos.filter(todo => todo.id !== id); return this.todos.length < initialLength; }
async findByCompleted(completed: boolean): Promise<Todo[]> { return this.todos.filter(todo => todo.completed === completed); }}
Service Layer
Section titled “Service Layer”What about business logic? That goes in services:
export interface ITodoService { createTodo(data: CreateTodoData): Promise<Todo>; getAllTodos(): Promise<Todo[]>; getTodoById(id: string): Promise<Todo | undefined>; updateTodo(id: string, data: UpdateTodoData): Promise<Todo | undefined>; deleteTodo(id: string): Promise<boolean>; markAsCompleted(id: string): Promise<Todo | undefined>; markAsPending(id: string): Promise<Todo | undefined>; getCompletedTodos(): Promise<Todo[]>; getPendingTodos(): Promise<Todo[]>;}
// services/TodoService.ts (singleton by default)export class TodoService implements ITodoService { constructor(private todoRepository: ITodoRepository) {}
async createTodo(data: CreateTodoData): Promise<Todo> { const todo = new Todo(data); return await this.todoRepository.create(todo); }
async getAllTodos(): Promise<Todo[]> { return await this.todoRepository.findAll(); }
async getTodoById(id: string): Promise<Todo | undefined> { return await this.todoRepository.findById(id); }
async updateTodo(id: string, data: UpdateTodoData): Promise<Todo | undefined> { return await this.todoRepository.update(id, data); }
async deleteTodo(id: string): Promise<boolean> { return await this.todoRepository.delete(id); }
async markAsCompleted(id: string): Promise<Todo | undefined> { const todo = await this.todoRepository.findById(id); if (todo) { todo.markAsCompleted(); return await this.todoRepository.update(id, { completed: true }); } return undefined; }
async markAsPending(id: string): Promise<Todo | undefined> { const todo = await this.todoRepository.findById(id); if (todo) { todo.markAsPending(); return await this.todoRepository.update(id, { completed: false }); } return undefined; }
async getCompletedTodos(): Promise<Todo[]> { return await this.todoRepository.findByCompleted(true); }
async getPendingTodos(): Promise<Todo[]> { return await this.todoRepository.findByCompleted(false); }}
Configuration
Section titled “Configuration”{ "source": ".", "output": "container.gen.ts"}
Direct Container Access
Section titled “Direct Container Access”import { container } from './container.gen';
const todoService = container.coreModule.ITodoService;const todoRepository = container.coreModule.ITodoRepository;
// Create a new todoawait todoService.createTodo({ title: 'Learn IoC Arise', description: 'Study dependency injection'});
// Get all todosconst todos = await todoService.getAllTodos();console.log('All todos:', todos);
Type-Safe inject() Function
Section titled “Type-Safe inject() Function”import { inject } from './container.gen';
// Access services with full type safetyconst todoService = inject('coreModule.ITodoService');
// Create todosawait todoService.createTodo({ title: 'Buy groceries', description: 'Milk, bread, eggs'});
await todoService.createTodo({ title: 'Walk the dog', description: 'Take Rex for a 30-minute walk'});
// Update a todoconst todos = await todoService.getAllTodos();if (todos.length > 0) { await todoService.updateTodo(todos[0].id, { title: 'Buy groceries - DONE', completed: true });}
// Get completed vs pending todosconst completedTodos = await todoService.getCompletedTodos();const pendingTodos = await todoService.getPendingTodos();
Container-Level Initialization
Section titled “Container-Level Initialization”The generated container includes an onInit()
function for custom setup:
// This function is exported from ./container.gen.ts// You can modify it there for any custom initializationimport { onInit } from './container.gen';
// onInit() is called automatically when inject() is first used// It's at the container level, NOT in individual classes
Key Benefits
Section titled “Key Benefits”- Simplicity: Easy to understand and extend with minimal configuration
- Interface-Based Design: Clean separation between contracts and implementations
- Type Safety: Full TypeScript support with interface-driven service keys
- Repository Pattern: Clean data access abstraction for easy testing and swapping
- Service Layer: Centralized business logic with dependency injection
- Zero Configuration: Just organize files and IoC Arise handles dependency resolution