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});