Skip to content

Simple Modules Example

This example demonstrates IoC Arise’s module system with two separate modules that have cross-module dependencies. You’ll learn how to organize your code into logical modules while maintaining type-safe dependency injection across module boundaries.

  • Directorysimple-modules/
    • Directoryuser/
      • User.ts User entity
      • IUserRepository.ts User repository interface
      • UserRepository.ts User repository implementation
      • IUserService.ts User service interface
      • UserService.ts User service implementation
    • Directorytodo/
      • Todo.ts Todo entity
      • ITodoRepository.ts Todo repository interface
      • TodoRepository.ts Todo repository implementation
      • ITodoService.ts Todo service interface
      • TodoService.ts Todo service implementation (cross-module dependency)
    • container.gen.ts Main container aggregating modules
    • demo.ts Demonstration of module interaction
    • ioc.config.json IoC configuration with module definitions
    • README.md This file

Each module encapsulates related functionality:

UserModule:

  • User entity with user data
  • IUserRepository & UserRepository for data access
  • IUserService & UserService for business logic

TodoModule:

  • Todo entity with todo data
  • ITodoRepository & TodoRepository for data access
  • ITodoService & TodoService for business logic

The TodoService demonstrates cross-module dependency by depending on UserRepository:

export class TodoService implements ITodoService {
constructor(
private todoRepository: ITodoRepository, // Same module (TodoModule)
private userRepository: IUserRepository, // Different module (UserModule)!
) {}
async createTodo(title: string, userId: string): Promise<Todo> {
// Validate user exists using cross-module dependency
const user = await this.userRepository.findById(userId);
if (!user) {
throw new Error(`User with ID ${userId} not found`);
}
// Create todo logic...
}
}

IoC Arise automatically:

  • Discovers dependencies across modules
  • Creates proper module instantiation order
  • Wires everything together in a unified container
import { container } from "./container.gen";
// Access user module services
const userService = container.userModule.IUserService;
const userRepository = container.userModule.IUserRepository;
// Access todo module services
const todoService = container.todoModule.ITodoService;
const todoRepository = container.todoModule.ITodoRepository;
// Use services
const users = await userService.getAllUsers();
const userTodos = await todoService.getTodosByUser("1");

Method 2: Using inject() Function (Type-Safe)

Section titled “Method 2: Using inject() Function (Type-Safe)”
import { inject } from "./container.gen";
// Access services with full type safety and module paths
const userService = inject("userModule.IUserService");
const todoService = inject("todoModule.ITodoService");
// Create a new todo (with user validation)
const newTodo = await todoService.createTodo("Learn IoC Arise", "1");
// Get all users
const users = await userService.getAllUsers();
// 1. Get all users
const users = await userService.getAllUsers();
console.log("Available users:", users);
// 2. Create a todo for a specific user
const newTodo = await todoService.createTodo("Complete project", "1");
console.log("Created todo:", newTodo);
// 3. Get todos for that user
const userTodos = await todoService.getTodosByUser("1");
console.log("User todos:", userTodos);
// 4. Update todo status
const updatedTodo = await todoService.updateTodo(newTodo.id, undefined, true);
console.log("Updated todo:", updatedTodo);
// 5. Try to create todo for non-existent user (will throw error)
try {
await todoService.createTodo("Invalid todo", "non-existent-user");
} catch (error) {
console.log("Expected error:", error.message);
}
export class User {
constructor(
public id: string,
public name: string,
public email: string,
) {}
}
export class Todo {
constructor(
public id: string,
public title: string,
public userId: string,
public completed: boolean = false,
) {}
}
export interface IUserService {
getAllUsers(): Promise<User[]>;
getUserById(id: string): Promise<User | undefined>;
createUser(name: string, email: string): Promise<User>;
updateUser(
id: string,
name?: string,
email?: string,
): Promise<User | undefined>;
deleteUser(id: string): Promise<boolean>;
}
export interface ITodoService {
getAllTodos(): Promise<Todo[]>;
getTodoById(id: string): Promise<Todo | undefined>;
getTodosByUser(userId: string): Promise<Todo[]>;
createTodo(title: string, userId: string): Promise<Todo>;
updateTodo(
id: string,
title?: string,
completed?: boolean,
): Promise<Todo | undefined>;
deleteTodo(id: string): Promise<boolean>;
}

The Todo module depends on the User module through the IUserRepository interface:

import { Todo } from "./Todo";
import { ITodoService } from "./ITodoService";
import { ITodoRepository } from "./ITodoRepository";
import { IUserRepository } from "../user/IUserRepository";
/**
* @scope singleton
*/
export class TodoService implements ITodoService {
constructor(
private todoRepository: ITodoRepository,
private userRepository: IUserRepository,
) {}
async getTodosByUser(userId: string): Promise<Todo[]> {
// Verify user exists before getting todos
const user = await this.userRepository.findById(userId);
if (!user) {
throw new Error(`User with id ${userId} not found`);
}
return this.todoRepository.findByUserId(userId);
}
async createTodo(title: string, userId: string): Promise<Todo> {
// Verify user exists before creating todo
const user = await this.userRepository.findById(userId);
if (!user) {
throw new Error(`User with id ${userId} not found`);
}
return this.todoRepository.create(title, userId);
}
// ... other methods
}

The onInit() function is exported from the generated container.gen.ts file, not a method in your classes:

import { onInit } from "./container.gen";
// The onInit function is called automatically when inject() is first used
// You can modify it in the generated container.gen.ts file for custom initialization

For services that need cross-module dependencies setup:

export class ApplicationCoordinator {
private userService!: IUserService;
private todoService!: ITodoService;
initializeCrossModuleDependencies() {
this.userService = inject("userModule.IUserService");
this.todoService = inject("todoModule.ITodoService");
console.log(
"ApplicationCoordinator initialized with cross-module dependencies",
);
}
async setupUserWithTodos(name: string, email: string, todoTitles: string[]) {
// Create user using UserModule service
const user = await this.userService.createUser(name, email);
// Create todos using TodoModule service (with cross-module dependency)
const todos = [];
for (const title of todoTitles) {
const todo = await this.todoService.createTodo(title, user.id);
todos.push(todo);
}
console.log(`Created user ${user.name} with ${todos.length} todos`);
return { user, todos };
}
async getUserStats(userId: string) {
const user = await this.userService.getUser(userId);
const userTodos = await this.todoService.getTodosByUser(userId);
return {
user: user?.name || "Unknown",
totalTodos: userTodos.length,
completedTodos: userTodos.filter((t) => t.completed).length,
};
}
}

The ioc.config.json defines modules and their patterns:

{
"source": ".",
"output": "container.gen.ts",
"modules": {
"UserModule": ["user/**/*"],
"TodoModule": ["todo/**/*"]
}
}

This configuration:

  • Groups files in user/ directory into UserModule
  • Groups files in todo/ directory into TodoModule
  • Automatically resolves cross-module dependencies
  • Generates separate module containers with proper dependency injection order
  1. Modular Organization: Keep related code together in logical modules
  2. Cross-Module Dependencies: Services can safely depend on other modules
  3. Type Safety: Full TypeScript support across module boundaries
  4. Testability: Easy to mock entire modules or individual services
  5. Scalability: Add new modules without affecting existing ones
  6. Clean Architecture: Clear separation of concerns between domains
  7. Dependency Validation: Automatic validation of cross-module dependencies

This example shows how IoC Arise’s module system enables clean separation of concerns while maintaining the ability to share functionality across domains, making it perfect for scalable applications!