Features
Docs
CLI
Benchmarks
Examples

© 2024 MoroJs

Dependency Injection

Manage services and dependencies with MoroJS's sophisticated dependency injection container. Support for multiple scopes, lifecycle hooks, interceptors, and advanced patterns.

Clean Architecture with DI

MoroJS includes a sophisticated dependency injection system for managing services, dependencies, and application architecture with support for multiple scopes and lifecycle hooks.

Quick Start

Basic Service Registration

typescript

1import { createApp } from '@morojs/moro';
2
3const app = 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?

Use Basic Container when you just need simple dependency injection without lifecycle management
Use Enhanced Container when you need service scopes, lifecycle hooks, timeouts, interceptors, or any advanced DI features
💡
Both APIs work together: You can register with enhanced and resolve with basic container, or mix both approaches

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.

ScopeLifetimeWhen to Use
SingletonOne instance for entire appDatabase, cache, config
TransientNew instance every timeRequest IDs, factories
RequestOne instance per HTTP requestUser context, tracing
ModuleOne instance per moduleModule-specific state

Singleton

A single instance is created and shared across the entire application. Perfect for stateful services like database connections, caches, and configuration.

DatabaseCacheConfigLogger
// 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); // true

Transient

A new instance is created every time the service is requested. Ideal for lightweight, stateless services and unique identifiers.

Request IDsFactoriesTemp ObjectsEvents
// 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); // false

Request Scope

One instance is created per HTTP request and shared across that request. Perfect for user context, request-specific state, and per-request logging.

User ContextRequest TraceAuthTransactions
// 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.

Module CacheModule ConfigStateMetrics
// 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 state

Service 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 = singleton

Class-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.

With Dependencies

typescript

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

With Lifecycle Hooks

typescript

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

Service Timeout and Fallback

typescript

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

Optional Dependencies

typescript

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

Service Interceptors

typescript

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

Service Decorators

typescript

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

Service Tags for Batch Resolution

typescript

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}

Async Service Initialization

typescript

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(): FunctionalContainer

Enhanced 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 register

Best 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();

Next Steps