Dependency Injection
Dependency Injection (DI) decouples components and improves testability. Learn DI patterns and frameworks for managing dependencies in Node.js applications.
Challenge
Section titled “Challenge”Implement Dependency Injection to create loosely coupled, testable, and maintainable applications.
Explanation
Section titled “Explanation”DI Principles
Section titled “DI Principles”Inversion of Control (IoC): Objects don’t create dependencies, they receive them Loose Coupling: Components depend on abstractions, not implementations Testability: Easy to mock dependencies for testing Maintainability: Changes to implementations don’t require changes to consumers
DI Patterns
Section titled “DI Patterns”Constructor Injection: Dependencies passed via constructor Setter Injection: Dependencies set via methods Interface Injection: Dependencies implement specific interface
Code Examples
Section titled “Code Examples”1. Constructor Injection
Section titled “1. Constructor Injection”class UserService { constructor(database, emailService, logger) { this.database = database; this.emailService = emailService; this.logger = logger; }
async createUser(userData) { this.logger.info('Creating user', userData);
try { const user = await this.database.users.create(userData); await this.emailService.sendWelcomeEmail(user.email); this.logger.info('User created successfully', { userId: user.id }); return user; } catch (error) { this.logger.error('Failed to create user', { error: error.message }); throw error; } }
async getUser(userId) { return this.database.users.findById(userId); }}
// Dependency Injection Containerclass Container { constructor() { this.services = new Map(); }
register(name, definition) { this.services.set(name, definition); }
get(name) { const definition = this.services.get(name); if (!definition) { throw new Error(`Service ${name} not found`); } return typeof definition === 'function' ? definition() : definition; }}
// Setupconst container = new Container();container.register('database', () => new Database());container.register('emailService', () => new EmailService());container.register('logger', () => console.log);
container.register('userService', () => new UserService( container.get('database'), container.get('emailService'), container.get('logger') ));
const userService = container.get('userService');2. InversifyJS Framework
Section titled “2. InversifyJS Framework”const { Container, injectable, inject } = require('inversify');
// Define service identifiersconst TYPES = { UserService: Symbol.for('UserService'), UserRepository: Symbol.for('UserRepository'), EmailService: Symbol.for('EmailService'), Logger: Symbol.for('Logger'),};
// Interfacesclass IUserRepository { async findById(id) {} async create(data) {}}
class IEmailService { async send(to, subject, body) {}}
// Implementations@injectable()class UserRepository extends IUserRepository { async findById(id) { return { id, name: 'John Doe' }; }
async create(data) { return { id: '1', ...data }; }}
@injectable()class EmailService extends IEmailService { async send(to, subject, body) { console.log(`Email sent to ${to}: ${subject}`); }}
@injectable()class UserService { constructor( @inject(TYPES.UserRepository) userRepository, @inject(TYPES.EmailService) emailService, @inject(TYPES.Logger) logger ) { this.userRepository = userRepository; this.emailService = emailService; this.logger = logger; }
async createUser(userData) { const user = await this.userRepository.create(userData); await this.emailService.send(user.email, 'Welcome!', '...'); return user; }}
// Container setupconst container = new Container();
container.bind(TYPES.UserRepository).to(UserRepository).inSingletonScope();container.bind(TYPES.EmailService).to(EmailService).inSingletonScope();container.bind(TYPES.Logger).toConstantValue(console.log);container.bind(TYPES.UserService).to(UserService);
const userService = container.get(TYPES.UserService);3. Factory Pattern for DI
Section titled “3. Factory Pattern for DI”class ServiceFactory { static createUserService(config = {}) { const database = new Database(config.dbUrl); const emailService = new EmailService(config.emailProvider); const logger = new Logger(config.logLevel);
return new UserService(database, emailService, logger); }
static createOrderService(config = {}) { const database = new Database(config.dbUrl); const paymentService = new PaymentService(config.paymentProvider); const notificationService = new NotificationService(config.notificationProvider);
return new OrderService(database, paymentService, notificationService); }}
// Usageconst userService = ServiceFactory.createUserService({ dbUrl: process.env.DATABASE_URL, emailProvider: 'sendgrid', logLevel: 'info',});4. Testing with DI
Section titled “4. Testing with DI”// Mock implementations for testingclass MockUserRepository { async findById(id) { return { id: '1', name: 'Test User', email: 'test@example.com' }; }
async create(data) { return { id: '1', ...data }; }}
class MockEmailService { async send(to, subject, body) { // Mock implementation console.log(`Mock email sent`); }}
// Test setupconst mockRepository = new MockUserRepository();const mockEmailService = new MockEmailService();const mockLogger = { info: jest.fn(), error: jest.fn(),};
const userService = new UserService( mockRepository, mockEmailService, mockLogger);
// Testdescribe('UserService', () => { it('should create a user and send welcome email', async () => { const user = await userService.createUser({ name: 'John', email: 'john@example.com', });
expect(user.name).toBe('John'); expect(mockLogger.info).toHaveBeenCalled(); });});5. Module Pattern for DI
Section titled “5. Module Pattern for DI”module.exports = (dependencies) => { const { database, emailService, logger } = dependencies;
return { async createUser(userData) { logger.info('Creating user', userData); const user = await database.users.create(userData); await emailService.sendWelcomeEmail(user.email); return user; },
async getUser(userId) { return database.users.findById(userId); },
async updateUser(userId, updates) { return database.users.update(userId, updates); }, };};
// app.jsconst userServiceModule = require('./services/userServiceModule');
const dependencies = { database: new Database(), emailService: new EmailService(), logger: console,};
const userService = userServiceModule(dependencies);6. Middleware with DI in Express
Section titled “6. Middleware with DI in Express”function createAuthMiddleware(tokenService, userRepository) { return async (req, res, next) => { const token = req.headers.authorization?.split(' ')[1];
if (!token) { return res.status(401).json({ error: 'No token provided' }); }
try { const payload = tokenService.verify(token); const user = await userRepository.findById(payload.userId);
if (!user) { return res.status(401).json({ error: 'User not found' }); }
req.user = user; next(); } catch (error) { res.status(401).json({ error: 'Invalid token' }); } };}
// Setupconst app = express();const tokenService = new TokenService();const userRepository = new UserRepository();
app.use(createAuthMiddleware(tokenService, userRepository));Best Practices
Section titled “Best Practices”-
Use Interfaces/Abstractions
- Define contracts between services
- Make mocking easier
- Improve code organization
-
Scope Management
- Singleton: One instance shared
- Transient: New instance each time
- Scoped: One instance per request
-
Configuration
- Externalize configuration
- Use environment variables
- Support multiple environments
-
Testing Strategy
- Create mock implementations
- Test in isolation
- Use DI for test setup
-
Documentation
- Document service dependencies
- Provide setup examples
- Maintain dependency graph
Real-World Applications
Section titled “Real-World Applications”- Enterprise Applications: Complex dependency management
- Microservices: Service discovery and registration
- Testing Frameworks: Inject mocks and stubs
- Plugin Systems: Dynamic service registration
- Multi-tenant SaaS: Per-tenant dependency configuration