Module System

Build scalable applications with MoroJS's powerful module system. Organize code, manage dependencies, and create reusable components.

Module Basics

MoroJS modules provide isolation, dependency injection, and clean separation of concerns. Each module has its own scope and can export services and routes.

Functional Module Structure

typescript

1// modules/users/index.ts - Functional Module Definition
2import { defineModule } from '@morojs/moro';
3import { routes } from './routes';
4import { sockets } from './sockets';
5import * as actions from './actions';
6import * as types from './types';
7
8export default defineModule({
9  name: 'users',
10  version: '1.0.0',
11  prefix: '/api/v1.0.0/users',
12  
13  // Register routes (functional approach)
14  routes,
15  
16  // Register WebSocket handlers
17  sockets,
18  
19  // Export module actions and types
20  actions,
21  types,
22  
23  // Module lifecycle hooks
24  onLoad: async (app) => {
25    console.log('👥 Users module loaded');
26    // Initialize any module-specific services here
27  },
28  
29  onUnload: async (app) => {
30    console.log('👥 Users module unloaded');
31  }
32});
33
34// Loading modules in app.ts
35import { createApp } from '@morojs/moro';
36import UsersModule from './modules/users';
37import OrdersModule from './modules/orders';
38import HealthModule from './modules/health';
39
40async function createEnterpriseApp() {
41  const app = createApp({
42    cors: true,
43    compression: true,
44    helmet: true
45  });
46
47  // Register database (in-memory for demo)
48  const mockDatabase = {
49    users: [
50      { id: 1, name: 'John Doe', email: 'john@example.com', role: 'admin' },
51      { id: 2, name: 'Jane Smith', email: 'jane@example.com', role: 'user' }
52    ]
53  };
54  app.database(mockDatabase);
55
56  // Load functional modules
57  await app.loadModule(HealthModule);
58  await app.loadModule(UsersModule);
59  await app.loadModule(OrdersModule);
60
61  return app;
62}

Module Benefits

  • • Isolated scope and dependencies
  • • Dependency injection container
  • • Reusable and testable components
  • • Clear separation of concerns
  • • Hot module replacement in development

Module Services

Actions Definition (Functional Approach)

typescript

1// modules/users/actions.ts - Pure Business Logic Functions
2import { User, CreateUserRequest, UpdateUserRequest } from './types';
3
4// Pure business logic functions that receive dependencies as parameters
5export async function getAllUsers(database: any): Promise<User[]> {
6  const mockUsers = [
7    { id: 1, name: 'John Doe', email: 'john@example.com', role: 'admin', active: true },
8    { id: 2, name: 'Jane Smith', email: 'jane@example.com', role: 'user', active: true }
9  ];
10  
11  // Production-ready database integration - currently using mock data
12  return database?.users || mockUsers;
13}
14
15export async function getUserById(id: number, database: any): Promise<User | null> {
16  const users = database.users || [];
17  return users.find((user: User) => user.id === id) || null;
18}
19
20export async function createUser(userData: CreateUserRequest, database: any, events: any): Promise<User> {
21  const users = database.users || [];
22  
23  // Validate user data
24  if (!userData.email || !userData.email.includes('@')) {
25    throw new Error('Invalid email address');
26  }
27  
28  // Check if email already exists
29  const existingUser = users.find((u: User) => u.email === userData.email);
30  if (existingUser) {
31    throw new Error('User with this email already exists');
32  }
33  
34  const newUser: User = {
35    id: Math.max(...users.map((u: User) => u.id), 0) + 1,
36    name: userData.name,
37    email: userData.email,
38    role: userData.role || 'user',
39    active: true,
40    createdAt: new Date(),
41    updatedAt: new Date()
42  };
43
44  users.push(newUser);
45  
46  // Emit user creation event
47  await events.emit('user.created', { user: newUser });
48  
49  return newUser;
50}
51
52export async function updateUser(id: number, updateData: UpdateUserRequest, database: any, events: any): Promise<User | null> {
53  const users = database.users || [];
54  const userIndex = users.findIndex((user: User) => user.id === id);
55  
56  if (userIndex === -1) {
57    return null;
58  }
59
60  const updates: any = {};
61  if (updateData.name !== undefined) updates.name = updateData.name;
62  if (updateData.email !== undefined) updates.email = updateData.email;
63  if (updateData.role !== undefined) updates.role = updateData.role;
64  
65  updates.updatedAt = new Date();
66
67  users[userIndex] = { ...users[userIndex], ...updates };
68  
69  // Emit user update event
70  await events.emit('user.updated', { user: users[userIndex], changes: updates });
71  
72  return users[userIndex];
73}
74
75// Type definitions
76interface User {
77  id: number;
78  email: string;
79  name: string;
80  role: string;
81  active: boolean;
82  createdAt: Date;
83  updatedAt: Date;
84}
85
86interface CreateUserRequest {
87  email: string;
88  name: string;
89  role?: string;
90}
91
92interface UpdateUserRequest {
93  email?: string;
94  name?: string;
95  role?: string;
96}

Module Routes

Module Route Definition

typescript

1// modules/users/routes.ts
2import { ModuleRouter, Inject } from '@morojs/moro';
3import { UserService } from './services/UserService';
4import { AuthService } from '../auth/services/AuthService';
5import { z } from 'zod';
6
7export const userRoutes = (router: ModuleRouter) => {
8  // Inject services
9  const userService = router.inject<UserService>('UserService');
10  const authService = router.inject<AuthService>('AuthService');
11  
12  // Get all users
13  router.get('/users', {
14    middleware: [authService.requireAuth],
15    handler: async () => {
16      const users = await userService.getAllUsers();
17      return users;
18    }
19  });
20  
21  // Get user by ID
22  router.get('/users/:id', {
23    params: z.object({
24      id: z.string().uuid()
25    }),
26    middleware: [authService.requireAuth],
27    handler: async ({ params }) => {
28      const user = await userService.getUserById(params.id);
29      if (!user) {
30        throw new Error('User not found');
31      }
32      return user;
33    }
34  });
35  
36  // Create new user
37  router.post('/users', {
38    body: z.object({
39      email: z.string().email(),
40      name: z.string().min(2).max(50)
41    }),
42    middleware: [authService.requireAuth, authService.requireRole('admin')],
43    handler: async ({ body }) => {
44      const user = await userService.createUser(body);
45      return user;
46    }
47  });
48  
49  // Update user
50  router.put('/users/:id', {
51    params: z.object({
52      id: z.string().uuid()
53    }),
54    body: z.object({
55      name: z.string().min(2).max(50).optional(),
56      email: z.string().email().optional()
57    }),
58    middleware: [
59      authService.requireAuth,
60      authService.requireOwnershipOrRole('admin')
61    ],
62    handler: async ({ params, body }) => {
63      const user = await userService.updateUser(params.id, body);
64      return user;
65    }
66  });
67  
68  // Delete user
69  router.delete('/users/:id', {
70    params: z.object({
71      id: z.string().uuid()
72    }),
73    middleware: [
74      authService.requireAuth,
75      authService.requireRole('admin')
76    ],
77    handler: async ({ params }) => {
78      await userService.deleteUser(params.id);
79      return { success: true };
80    }
81  });
82};

Functional Dependency Management

Functional Service Management

typescript

1// modules/shared/services.ts - Functional Service Pattern
2// Service factory functions instead of decorators
3export function createUserRepository(database: any) {
4  return {
5    async findById(id: number): Promise<User | null> {
6      const users = database.users || [];
7      return users.find((user: User) => user.id === id) || null;
8    },
9    
10    async findByEmail(email: string): Promise<User | null> {
11      const users = database.users || [];
12      return users.find((user: User) => user.email === email) || null;
13    },
14    
15    async create(userData: CreateUserRequest): Promise<User> {
16      const users = database.users || [];
17      const newUser = {
18        id: Math.max(...users.map((u: User) => u.id), 0) + 1,
19        ...userData,
20        createdAt: new Date(),
21        updatedAt: new Date()
22      };
23      users.push(newUser);
24      return newUser;
25    }
26  };
27}
28
29// Cache service factory
30export function createCacheService() {
31  const cache = new Map<string, any>();
32  
33  return {
34    get(key: string): any {
35      return cache.get(key);
36    },
37    
38    set(key: string, value: any, ttl?: number): void {
39      cache.set(key, value);
40      
41      if (ttl) {
42        setTimeout(() => {
43          cache.delete(key);
44        }, ttl * 1000);
45      }
46    },
47    
48    delete(key: string): boolean {
49      return cache.delete(key);
50    },
51    
52    clear(): void {
53      cache.clear();
54    }
55  };
56}
57
58// Service composition in module
59export function createUserServices(dependencies: {
60  database: any;
61  cache?: any;
62  events?: any;
63}) {
64  const userRepository = createUserRepository(dependencies.database);
65  const cacheService = dependencies.cache || createCacheService();
66  
67  return {
68    userRepository,
69    cacheService,
70    
71    // Combined service methods
72    async getCachedUser(id: number): Promise<User | null> {
73      const cacheKey = `user:${id}`;
74      let user = cacheService.get(cacheKey);
75      
76      if (!user) {
77        user = await userRepository.findById(id);
78        if (user) {
79          cacheService.set(cacheKey, user, 300); // 5 minutes
80        }
81      }
82      
83      return user;
84    }
85  };
86}

Module Routes Definition

typescript

1// modules/users/routes.ts - Functional Route Definitions
2import { z, validate } from '@morojs/moro';
3import * as actions from './actions';
4
5export const routes = [
6  {
7    method: 'GET' as const,
8    path: '/',
9    validation: {
10      query: z.object({
11        page: z.coerce.number().default(1),
12        limit: z.coerce.number().max(100).default(20),
13        search: z.string().optional(),
14        role: z.string().optional()
15      })
16    },
17    cache: { ttl: 60 },
18    rateLimit: { requests: 100, window: 60000 },
19    description: 'Get all users with pagination and filtering',
20    tags: ['users', 'list'],
21    handler: async (req: any, res: any) => {
22      const database = req.database;
23      const users = await actions.getAllUsers(database);
24      
25      // Apply query filtering
26      let filteredUsers = users;
27      if (req.query.role) {
28        filteredUsers = filteredUsers.filter((user: any) => user.role === req.query.role);
29      }
30      if (req.query.search) {
31        filteredUsers = filteredUsers.filter((user: any) => 
32          user.name.toLowerCase().includes(req.query.search.toLowerCase()) ||
33          user.email.toLowerCase().includes(req.query.search.toLowerCase())
34        );
35      }
36      
37      // Apply pagination
38      const { limit, offset } = req.query;
39      const paginatedUsers = filteredUsers.slice(Number(offset || 0), Number(offset || 0) + Number(limit));
40      
41      return { 
42        success: true, 
43        data: paginatedUsers,
44        pagination: {
45          total: filteredUsers.length,
46          limit,
47          offset: offset || 0,
48          hasMore: Number(offset || 0) + Number(limit) < filteredUsers.length
49        }
50      };
51    }
52  },
53  
54  {
55    method: 'POST' as const,
56    path: '/',
57    validation: {
58      body: z.object({
59        name: z.string().min(2).max(50),
60        email: z.string().email(),
61        role: z.enum(['admin', 'user']).default('user')
62      })
63    },
64    rateLimit: { requests: 20, window: 60000 },
65    description: 'Create a new user',
66    tags: ['users', 'create'],
67    handler: async (req: any, res: any) => {
68      const database = req.database;
69      const events = req.events || { emit: async () => {} };
70      
71      const user = await actions.createUser(req.body, database, events);
72      
73      res.status(201);
74      return { success: true, data: user };
75    }
76  }
77];

Module Communication

Event-Driven Module Communication (Functional)

typescript

1// modules/users/sockets.ts - WebSocket Event Handlers
2export const sockets = [
3  {
4    event: 'user.join',
5    handler: async (socket: any, data: any, context: any) => {
6      const { roomId, userId } = data;
7      
8      // Join user to room
9      socket.join(`user-${userId}`);
10      
11      // Broadcast user joined event
12      socket.broadcast.emit('user.joined', {
13        userId,
14        roomId,
15        timestamp: new Date()
16      });
17      
18      return { success: true, message: 'User joined successfully' };
19    }
20  },
21  
22  {
23    event: 'user.update',
24    handler: async (socket: any, data: any, context: any) => {
25      const { userId, updates } = data;
26      const database = context.database;
27      const events = context.events;
28      
29      // Update user data
30      const updatedUser = await actions.updateUser(userId, updates, database, events);
31      
32      if (updatedUser) {
33        // Broadcast to user's room
34        socket.to(`user-${userId}`).emit('user.updated', {
35          user: updatedUser,
36          timestamp: new Date()
37        });
38      }
39      
40      return { success: !!updatedUser, user: updatedUser };
41    }
42  }
43];
44
45// Event handling in actions (functional approach)
46export async function handleUserEvents(eventName: string, eventData: any, services: any) {
47  switch (eventName) {
48    case 'user.created':
49      // Send welcome email (if email service available)
50      if (services.emailService) {
51        await services.emailService.sendWelcomeEmail(eventData.user.email);
52      }
53      
54      // Log audit event (if audit service available)
55      if (services.auditService) {
56        await services.auditService.log('user_created', {
57          userId: eventData.user.id,
58          email: eventData.user.email,
59          timestamp: new Date()
60        });
61      }
62      break;
63      
64    case 'user.updated':
65      // Log changes
66      if (services.auditService) {
67        await services.auditService.log('user_updated', {
68          userId: eventData.user.id,
69          changes: eventData.changes,
70          timestamp: new Date()
71        });
72      }
73      break;
74      
75    case 'user.deleted':
76      // Clean up user data
77      await cleanupUserData(eventData.userId, services);
78      
79      // Log deletion
80      if (services.auditService) {
81        await services.auditService.log('user_deleted', {
82          userId: eventData.userId,
83          timestamp: new Date()
84        });
85      }
86      break;
87  }
88}
89
90async function cleanupUserData(userId: number, services: any) {
91  // Remove user from other services
92  // This could emit events to other modules
93  if (services.notificationService) {
94    await services.notificationService.removeUserNotifications(userId);
95  }
96}

Module Testing

Module Unit Testing

typescript

1// modules/users/__tests__/UserService.test.ts
2import { createTestingModule } from '@morojs/moro/testing';
3import { UserService } from '../services/UserService';
4import { DatabaseService } from '../../database/services/DatabaseService';
5import { EventBus } from '@morojs/moro/events';
6
7describe('UserService', () => {
8  let userService: UserService;
9  let mockDb: jest.Mocked<DatabaseService>;
10  let mockEvents: jest.Mocked<EventBus>;
11  
12  beforeEach(async () => {
13    const testingModule = await createTestingModule({
14      providers: [UserService],
15      mocks: [
16        { provide: 'DatabaseService', useValue: createMockDatabase() },
17        { provide: 'EventBus', useValue: createMockEventBus() }
18      ]
19    });
20    
21    userService = testingModule.get<UserService>('UserService');
22    mockDb = testingModule.get('DatabaseService');
23    mockEvents = testingModule.get('EventBus');
24  });
25  
26  describe('createUser', () => {
27    it('should create a user successfully', async () => {
28      const userData = { email: 'test@example.com', name: 'Test User' };
29      const expectedUser = { id: '1', ...userData, created_at: new Date() };
30      
31      mockDb.query.mockResolvedValueOnce([expectedUser]);
32      
33      const result = await userService.createUser(userData);
34      
35      expect(result).toEqual(expectedUser);
36      expect(mockDb.query).toHaveBeenCalledWith(
37        expect.stringContaining('INSERT INTO users'),
38        [userData.email, userData.name, expect.any(Date)]
39      );
40      expect(mockEvents.emit).toHaveBeenCalledWith('user.created', { user: expectedUser });
41    });
42    
43    it('should throw error for duplicate email', async () => {
44      const userData = { email: 'existing@example.com', name: 'Test User' };
45      
46      // Mock existing user check
47      mockDb.query.mockResolvedValueOnce([{ id: '1' }]);
48      
49      await expect(userService.createUser(userData)).rejects.toThrow('User already exists');
50    });
51  });
52});
53
54// Mock factories
55function createMockDatabase(): jest.Mocked<DatabaseService> {
56  return {
57    query: jest.fn(),
58    transaction: jest.fn(),
59    close: jest.fn()
60  };
61}
62
63function createMockEventBus(): jest.Mocked<EventBus> {
64  return {
65    emit: jest.fn(),
66    on: jest.fn(),
67    off: jest.fn(),
68    once: jest.fn()
69  };
70}

Integration Testing

typescript

1// modules/users/__tests__/integration/users.integration.test.ts
2import { createTestApp } from '@morojs/moro/testing';
3import { usersModule } from '../index';
4import { databaseModule } from '../../database';
5import { authModule } from '../../auth';
6import request from 'supertest';
7
8describe('Users Module Integration', () => {
9  let app: any;
10  let server: any;
11  
12  beforeAll(async () => {
13    app = await createTestApp({
14      modules: [databaseModule, authModule, usersModule],
15      testDatabase: true // Use test database
16    });
17    
18    server = app.listen(0); // Random port
19  });
20  
21  afterAll(async () => {
22    await app.close();
23    server.close();
24  });
25  
26  beforeEach(async () => {
27    // Clean database before each test
28    await app.get('DatabaseService').query('TRUNCATE TABLE users CASCADE');
29  });
30  
31  describe('POST /users', () => {
32    it('should create a user with valid data', async () => {
33      const userData = {
34        email: 'test@example.com',
35        name: 'Test User'
36      };
37      
38      const response = await request(server)
39        .post('/users')
40        .send(userData)
41        .set('Authorization', 'Bearer valid-admin-token')
42        .expect(201);
43      
44      expect(response.body).toMatchObject({
45        email: userData.email,
46        name: userData.name,
47        id: expect.any(String),
48        created_at: expect.any(String)
49      });
50    });
51    
52    it('should return 401 without authentication', async () => {
53      const userData = {
54        email: 'test@example.com',
55        name: 'Test User'
56      };
57      
58      await request(server)
59        .post('/users')
60        .send(userData)
61        .expect(401);
62    });
63  });
64});

Next Steps