正在加载,请稍候…

Dependency Injection and IoC Containers in Node.js

Implement dependency injection in Node.js applications. Compare manual DI, tsyringe, InversifyJS, and NestJS IoC containers with practical examples.

Dependency Injection and IoC Containers in Node.js

Dependency injection decouples components by providing dependencies from outside.

Manual Dependency Injection

// Define interfaces (abstractions)
interface Logger {
  log(message: string): void;
  error(message: string, error?: Error): void;
}

interface UserRepository {
  findById(id: string): Promise<User | null>;
  save(user: User): Promise<void>;
}

// Concrete implementations
class ConsoleLogger implements Logger {
  log(message: string) { console.log(`[INFO] ${message}`); }
  error(message: string, error?: Error) { console.error(`[ERROR] ${message}`, error); }
}

class PostgresUserRepository implements UserRepository {
  constructor(private db: Database) {}
  async findById(id: string) { return this.db.users.findOne(id); }
  async save(user: User) { await this.db.users.upsert(user); }
}

// Service depends on abstractions
class UserService {
  constructor(
    private userRepo: UserRepository,
    private logger: Logger
  ) {}

  async getUser(id: string): Promise<User> {
    this.logger.log(`Getting user ${id}`);
    const user = await this.userRepo.findById(id);
    if (!user) throw new UserNotFoundError(id);
    return user;
  }
}

// Composition root - wire everything together
const logger = new ConsoleLogger();
const db = new Database(config.databaseUrl);
const userRepo = new PostgresUserRepository(db);
const userService = new UserService(userRepo, logger);

tsyringe (Lightweight Container)

import 'reflect-metadata';
import { container, injectable, inject, singleton } from 'tsyringe';

@singleton()
class ConsoleLogger implements Logger {
  log(message: string) { console.log(message); }
  error(message: string) { console.error(message); }
}

@injectable()
class UserRepository {
  constructor(@inject('Database') private db: Database) {}
  async findById(id: string) { return this.db.users.findOne(id); }
}

@injectable()
class UserService {
  constructor(
    private userRepo: UserRepository,
    private logger: ConsoleLogger
  ) {}

  async getUser(id: string) {
    this.logger.log(`Getting user ${id}`);
    return this.userRepo.findById(id);
  }
}

// Register and resolve
container.register('Database', { useValue: new Database(config.url) });
const service = container.resolve(UserService);

NestJS Built-in IoC

import { Injectable, Module } from '@nestjs/common';

@Injectable()
class UserRepository {
  async findById(id: string): Promise<User> { /* ... */ }
}

@Injectable()
class UserService {
  constructor(private userRepository: UserRepository) {}

  async getUser(id: string) {
    return this.userRepository.findById(id);
  }
}

@Module({
  providers: [UserService, UserRepository],
  exports: [UserService],
})
class UserModule {}

Custom Decorators for DI

// Simple decorator-based DI
const METADATA_KEY = Symbol('injectable');
const container = new Map<string, any>();

function Injectable(token: string) {
  return (target: any) => {
    Reflect.defineMetadata(METADATA_KEY, token, target);
  };
}

function register(token: string, implementation: any) {
  container.set(token, new implementation());
}

function resolve<T>(token: string): T {
  const instance = container.get(token);
  if (!instance) throw new Error(`No binding for ${token}`);
  return instance;
}

Testing with DI

// Easy to swap implementations in tests
describe('UserService', () => {
  let service: UserService;
  let mockRepo: jest.Mocked<UserRepository>;

  beforeEach(() => {
    mockRepo = {
      findById: jest.fn(),
      save: jest.fn(),
    };
    service = new UserService(mockRepo, new ConsoleLogger());
  });

  test('throws when user not found', async () => {
    mockRepo.findById.mockResolvedValue(null);
    await expect(service.getUser('unknown')).rejects.toThrow(UserNotFoundError);
  });
});

DI makes code testable, modular, and flexible—replacing components without changing consumers.