Skip to main content

Core Concepts 🧠

This section covers the fundamental concepts of dependency injection and NexusDI. We'll explain the key principles and components that make DI work.

What is Dependency Injection?

Dependency Injection (DI) is a design pattern where dependencies are provided to a class from the outside, rather than the class creating them internally. This promotes loose coupling, testability, and maintainability.

For a comprehensive explanation of DI principles, patterns, and benefits, see Dependency Injection.

Key Principles

1. Inversion of Control (IoC)

Instead of classes controlling their dependencies, dependencies are injected from outside:

// Traditional approach
class UserService {
private database = new PostgresDatabase(); // Class controls dependencies
}

// DI approach
@Service(USER_SERVICE)
class UserService {
constructor(@Inject(DATABASE) private database: IDatabase) {} // Dependencies injected
}

2. Dependency on Abstractions

Always depend on interfaces, not concrete implementations:

// ✅ Good - depends on interface
interface IDatabase {
query(sql: string, params: any[]): Promise<any>;
}

@Service(USER_SERVICE)
class UserService {
constructor(@Inject(DATABASE) private database: IDatabase) {} // Interface-based
}

// ❌ Bad - depends on concrete implementation
class UserService {
constructor(private database: PostgresDatabase) {} // Concrete class
}

For detailed explanations of why interfaces and tokens are better, see Dependency Injection.

Core Components

Tokens

Tokens are unique identifiers for dependencies with type safety:

export const USER_SERVICE = new Token<IUserService>('USER_SERVICE');
export const DATABASE = new Token<IDatabase>('DATABASE');

For a complete guide to tokens, see Tokens.

Providers

Providers define how dependencies are created:

// Class provider
nexus.set(USER_SERVICE, { useClass: UserService });

// Value provider
nexus.set(LOGGER, { useValue: new ConsoleLogger() });

// Factory provider
nexus.set(DATABASE, {
useFactory: (config: IConfig) => new Database(config),
deps: [CONFIG],
});

Services

Services are classes that can be injected with dependencies:

@Service(USER_SERVICE)
class UserService implements IUserService {
constructor(
@Inject(DATABASE) private database: IDatabase,
@Inject(LOGGER) private logger: ILogger
) {}
}

Benefits of DI

  • Loose coupling: Dependencies are abstracted through interfaces
  • Easy testing: Simple to mock dependencies
  • Flexible configuration: Easy to switch implementations
  • Modular architecture: Easy to compose and reuse components
  • Environment-specific setups: Different configs for dev/staging/prod

When to Use DI

Use Dependency Injection when you have:

  • Complex applications with many dependencies
  • High testing requirements
  • Multiple environments (dev/staging/prod)
  • Team development with multiple developers
  • Long-term maintenance needs

For simple applications or quick prototypes, regular imports may be more appropriate. Sometimes you just need to MacGyver a solution with what you have, not build a full workshop.

For a detailed comparison of DI vs regular imports, see DI vs Regular Imports.

Next Steps

Take your time to understand these concepts - they form the foundation of everything else! Remember, in the world of DI, you're not just a developer, you're a dependency warlock! 🌱