Advanced
Dependency Injection
Manage services and dependencies with MoroJS's sophisticated dependency injection container. Support for multiple scopes, lifecycle hooks, interceptors, and advanced patterns.
Overview
MoroJS includes a sophisticated dependency injection system for managing services, dependencies, and application architecture with support for multiple scopes and lifecycle hooks.
1import { createApp } from '@morojs/moro';
2
3const app = await createApp();
4const container = app.getContainer();
5
6// Register a simple service
7container.register('logger', () => {
8 return {
9 log: (message: string) => console.log(message)
10 };
11}, true); // true = singleton
12
13// Resolve and use the service
14const logger = container.resolve('logger');
15logger.log('Hello from DI!');Loose Coupling
Services depend on interfaces, not implementations. Easy to swap dependencies.
Testability
Mock dependencies easily for unit testing without changing code.
Lifecycle Management
Control service lifecycle with scopes, initialization, and cleanup hooks.
Container APIs: Basic vs Enhanced
MoroJS provides two APIs for working with dependency injection: the Basic Container and the Enhanced Container. Choose based on your needs for simplicity versus advanced features.
Basic Container
Simple and direct
Direct registration with simple factory functions. Great for straightforward use cases and quick setup.
Best for:
- • Quick prototypes
- • Simple applications
- • When you don't need advanced features
- • Direct, straightforward DI
const container = app.getContainer();
// Simple registration
container.register('logger', () => {
return console;
});
// Resolve
const logger = container.resolve('logger');Enhanced Container
Powerful and flexible
Chainable method calls that read like natural language. Includes advanced features like lifecycle hooks, scopes, timeouts, interceptors, and more.
Best for:
- • Production applications
- • Complex dependency graphs
- • Lifecycle management
- • Service scopes (singleton, transient, etc.)
const container = app.getContainer();
const enhanced = container.getEnhanced();
// Advanced registration with chainable methods
enhanced
.register('logger')
.factory(() => console)
.singleton()
.onInit((service) => {
service.log('Logger initialized');
})
.build();
// Resolve (same way)
const logger = container.resolve('logger');Quick Reference: Which Should I Use?
Service Scopes
MoroJS supports four different service scopes to control how and when service instances are created. Choose the right scope based on your service's lifecycle requirements.
| Scope | Lifetime | When to Use |
|---|---|---|
| Singleton | One instance for entire app | Database, cache, config |
| Transient | New instance every time | Request IDs, factories |
| Request | One instance per HTTP request | User context, tracing |
| Module | One instance per module | Module-specific state |
Singleton
A single instance is created and shared across the entire application. Perfect for stateful services like database connections, caches, and configuration.
// Get the enhanced container from your app
const container = app.getContainer();
const enhanced = container.getEnhanced();
// Register as singleton
enhanced
.register('cache')
.factory(() => new Map())
.singleton()
.build();
// Same instance everywhere
const cache1 = container.resolve('cache');
const cache2 = container.resolve('cache');
console.log(cache1 === cache2); // trueTransient
A new instance is created every time the service is requested. Ideal for lightweight, stateless services and unique identifiers.
// Get the enhanced container
const container = app.getContainer();
const enhanced = container.getEnhanced();
// Register as transient
enhanced
.register('requestId')
.factory(() => crypto.randomUUID())
.transient()
.build();
// Different instance each time
const id1 = container.resolve('requestId');
const id2 = container.resolve('requestId');
console.log(id1 === id2); // falseRequest Scope
One instance is created per HTTP request and shared across that request. Perfect for user context, request-specific state, and per-request logging.
// Get the enhanced container
const container = app.getContainer();
const enhanced = container.getEnhanced();
// Register with request scope
enhanced
.register('requestContext')
.factory((deps, context) => ({
userId: context?.userId,
timestamp: Date.now()
}))
.scoped('request')
.build();
// Use in your routes
app.get('/data', async (req, res) => {
const ctx = await enhanced.resolve('requestContext', {
context: { userId: req.user?.id }
});
// Same instance within this request
});Module Scope
One instance is created per module for isolation. Great for module-specific caches, state, and services that shouldn't be shared globally.
// Get the enhanced container
const container = app.getContainer();
const enhanced = container.getEnhanced();
// Register with module scope
enhanced
.register('moduleCache')
.factory(() => new Map())
.scoped('module')
.build();
// Each module gets its own instance
// userModule → cache A
// productModule → cache B
// They don't share stateService Registration
Accessing the Container
The container is accessible in different contexts throughout your application. Choose the pattern that best fits your architecture.
In Routes
Via req.app
Access the container through the request object in any route handler.
app.get('/users', async (req, res) => {
const container = req.app.getContainer();
const db = container.resolve('database');
const users = await db.query('SELECT * FROM users');
res.json(users);
});Singleton Export
Recommended
Export the container from a central file for use anywhere.
// container.ts
export const container = app.getContainer();
// Anywhere in your app
import { container } from './container';
const db = container.resolve('database');Pass App Instance
For modules
Pass the app instance to modules and access the container there.
// routes/users.ts
export function userRoutes(app: any) {
const container = app.getContainer();
const db = container.resolve('database');
// ... setup routes
}In Middleware
Via req.app
Access services in middleware through the request object.
export function authMiddleware(app: any) {
return async (req, res, next) => {
const container = req.app.getContainer();
const auth = container.resolve('authService');
// ... auth logic
};
}Registration Methods
Basic Registration
Simple function-based registration. Perfect for quick setup and small applications.
container.register('logger', () => {
return {
log: (msg) => console.log(msg)
};
}, true); // true = singletonClass-Based
Register class instances with proper dependency injection and lifecycle management.
container.register('database', () => {
return new DatabaseService({
host: 'localhost',
port: 5432
});
}, true);Enhanced Container (Chainable Methods)
The enhanced container provides a powerful chainable methods for advanced service registration with dependencies, lifecycle hooks, and more.
1const enhanced = container.getEnhanced();
2
3enhanced
4 .register('emailService')
5 .factory((deps) => {
6 const mailer = deps.mailer;
7 return {
8 sendEmail: async (to: string, subject: string, body: string) => {
9 await mailer.send({ to, subject, body });
10 }
11 };
12 })
13 .dependencies(['mailer'])
14 .singleton()
15 .tags(['email', 'messaging'])
16 .build();1enhanced
2 .register('databaseConnection')
3 .factory(() => ({
4 connection: null,
5 async connect() { /* ... */ },
6 async disconnect() { /* ... */ }
7 }))
8 .lifecycle({
9 init: async () => {
10 const db = container.resolve('databaseConnection');
11 await db.connect();
12 },
13 dispose: async () => {
14 const db = container.resolve('databaseConnection');
15 await db.disconnect();
16 }
17 })
18 .singleton()
19 .build();Advanced Features
1enhanced
2 .register('externalApi')
3 .factory(async () => {
4 // Potentially slow initialization
5 const response = await fetch('https://api.example.com/config');
6 return await response.json();
7 })
8 .timeout(5000) // 5 second timeout
9 .fallback(() => ({ default: 'config' })) // Fallback if timeout
10 .singleton()
11 .build();1enhanced
2 .register('notificationService')
3 .factory((deps) => {
4 const email = deps.emailService; // Required
5 const sms = deps.smsService; // Optional
6
7 return {
8 async notify(user: any, message: string) {
9 await email.send(user.email, message);
10 if (sms) {
11 await sms.send(user.phone, message);
12 }
13 }
14 };
15 })
16 .dependencies(['emailService'])
17 .optional(['smsService'])
18 .singleton()
19 .build();1enhanced
2 .register('userService')
3 .factory(() => new UserService())
4 .interceptor((name, factory, deps, context) => {
5 return async () => {
6 console.log(`Creating service: ${name}`);
7 const startTime = Date.now();
8 const service = await factory();
9 console.log(`Service ${name} created in ${Date.now() - startTime}ms`);
10 return service;
11 };
12 })
13 .singleton()
14 .build();1enhanced
2 .register('apiService')
3 .factory(() => ({
4 getData: async () => {
5 return { data: 'example' };
6 }
7 }))
8 .decorator(async (service, context) => {
9 // Wrap all methods with error handling
10 return new Proxy(service, {
11 get(target, prop) {
12 const original = target[prop];
13 if (typeof original === 'function') {
14 return async (...args: any[]) => {
15 try {
16 return await original.apply(target, args);
17 } catch (error) {
18 console.error(`Error in ${String(prop)}:`, error);
19 throw error;
20 }
21 };
22 }
23 return original;
24 }
25 });
26 })
27 .singleton()
28 .build();1// Register multiple services with tags
2enhanced
3 .register('postgresPlugin')
4 .factory(() => new PostgresPlugin())
5 .tags(['plugin', 'database'])
6 .singleton()
7 .build();
8
9enhanced
10 .register('redisPlugin')
11 .factory(() => new RedisPlugin())
12 .tags(['plugin', 'cache'])
13 .singleton()
14 .build();
15
16// Resolve all services with specific tag
17const plugins = enhanced.resolveByTag('plugin');
18for (const plugin of plugins) {
19 await plugin.initialize();
20}1enhanced
2 .register('database')
3 .factory(async (deps) => {
4 const config = deps.config;
5 const connection = await createConnection(config.database);
6 await connection.connect();
7 return connection;
8 })
9 .dependencies(['config'])
10 .singleton()
11 .build();
12
13// Resolve with async support
14const db = await enhanced.resolve('database');API Reference
Container Methods
// Basic Container
container.register<T>(name: string, factory: () => T, singleton?: boolean): void
container.resolve<T>(name: string): T
container.has(name: string): boolean
container.getEnhanced(): FunctionalContainerEnhanced Container Methods
// Enhanced Container (Fluent API)
enhanced.register<T>(name: string): ServiceRegistrationBuilder<T>
enhanced.resolve<T>(name: string, options?): Promise<T>
enhanced.resolveSync<T>(name: string, context?): T
enhanced.resolveByTag(tag: string): any[]
enhanced.dispose(): Promise<void>ServiceRegistrationBuilder Methods
builder.factory(fn) // Set factory function
builder.dependencies(deps[]) // Required dependencies
builder.optional(deps[]) // Optional dependencies
builder.singleton() // Singleton scope
builder.transient() // Transient scope
builder.scoped(scope) // Custom scope
builder.tags(tags[]) // Add tags
builder.lifecycle(hooks) // Add lifecycle hooks
builder.timeout(ms) // Set timeout
builder.fallback(fn) // Set fallback
builder.interceptor(fn) // Add interceptor
builder.decorator(fn) // Add decorator
builder.build() // Build and registerBest Practices
Use Singletons for Shared Resources
// Good: Singleton for database connection
container.register('database', () => createConnection(), true);
// Bad: Transient for database (creates many connections)
container.register('database', () => createConnection(), false);Inject Dependencies, Don't Import
// Good: Use dependency injection
class UserService {
constructor(private db: DatabaseService) {}
}
// Bad: Direct import creates tight coupling
import { database } from './database';
class UserService {
getData() {
return database.query('...');
}
}Use Interfaces for Flexibility
interface IEmailService {
send(to: string, subject: string, body: string): Promise<void>;
}
class SmtpEmailService implements IEmailService {
async send(to: string, subject: string, body: string) {
// SMTP implementation
}
}Lifecycle Management
// Always provide cleanup for resources
enhanced
.register('queueConnection')
.factory(() => new QueueConnection())
.lifecycle({
init: async () => {
const queue = container.resolve('queueConnection');
await queue.connect();
},
dispose: async () => {
const queue = container.resolve('queueConnection');
await queue.disconnect();
}
})
.singleton()
.build();