Registration
Goal: Create a provider and register it in a certain scope.
Registrations connect providers to the container with keys and configuration. They can be created using decorators or the fluent API, providing flexibility in how you organize your dependency configuration.
Registration Flow
The following diagram shows how registrations are processed and applied to containers:
bindToKey, pipe, when] Config --> CreateProvider[Registration creates provider with configuration] CreateProvider --> AddToContainer[Developer addRegistration to container] AddToContainer --> Apply[Container applies Registration] Apply --> CheckScope{Scope matches?} CheckScope -->|Yes| Register[Register provider & alias] CheckScope -->|No| Skip[Skip registration] Register --> Complete([Registration complete]) Skip --> Complete classDef startEnd fill:#4A90E2,stroke:#2E5C8A,stroke-width:3px,color:#fff classDef process fill:#6C5CE7,stroke:#4834D4,stroke-width:2px,color:#fff classDef config fill:#00B894,stroke:#00A085,stroke-width:2px,color:#fff classDef decision fill:#FDCB6E,stroke:#E17055,stroke-width:2px,color:#2D3436 classDef success fill:#00B894,stroke:#00A085,stroke-width:2px,color:#fff classDef skip fill:#636E72,stroke:#2D3436,stroke-width:2px,color:#fff class Start,Complete startEnd class CreateProvider,AddToContainer,Apply process class Config config class CheckScope decision class Register success class Skip skip
Registration Methods
There are several ways to create and apply registrations:
- Decorator-based: Using
@registerdecorator - Fluent API: Using
Registrationclass methods - Direct registration: Using
container.register()
Decorator-Based Registration
The @register decorator provides a declarative way to register classes. This is the most convenient approach for most use cases.
import {
bindTo,
Container,
DependencyMissingKeyError,
register,
Registration as R,
scope,
select as s,
singleton,
} from 'ts-ioc-container';
/**
* User Management Domain - Registration Patterns
*
* Registrations define how dependencies are bound to the container.
* Common patterns:
* - Register by class (auto-generates key from class name)
* - Register by value (constants, configuration)
* - Register by factory function (dynamic creation)
* - Register with aliases (multiple keys for same service)
*
* This is the foundation for dependency injection - telling the container
* "when someone asks for X, give them Y".
*/
describe('Registration module', function () {
const createAppContainer = () => new Container({ tags: ['application'] });
it('should register class with scope and lifecycle', function () {
// Logger is registered at application scope as a singleton
@register(bindTo('ILogger'), scope((s) => s.hasTag('application')), singleton())
class Logger {}
const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));
expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
});
it('should register configuration value', function () {
// Register application configuration as a value
const appContainer = createAppContainer().addRegistration(R.fromValue('production').bindToKey('Environment'));
expect(appContainer.resolve('Environment')).toBe('production');
});
it('should register factory function', function () {
// Factory functions are useful for dynamic creation
const appContainer = createAppContainer().addRegistration(
R.fromFn(() => `app-${Date.now()}`).bindToKey('RequestId'),
);
expect(appContainer.resolve('RequestId')).toContain('app-');
});
it('should raise an error if binding key is not provided', () => {
// Values and functions must have explicit keys (classes use class name by default)
expect(() => {
createAppContainer().addRegistration(R.fromValue('orphan-value'));
}).toThrowError(DependencyMissingKeyError);
});
it('should register dependency by class name when no key decorator is used', function () {
// Without @register(bindTo('key')), the class name becomes the key
class FileLogger {}
const appContainer = createAppContainer().addRegistration(R.fromClass(FileLogger));
expect(appContainer.resolve('FileLogger')).toBeInstanceOf(FileLogger);
});
it('should register with multiple keys using aliases', function () {
// Same service accessible via direct key and alias
@register(bindTo('ILogger'), bindTo(s.alias('Logger')), singleton())
class Logger {}
const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));
// Accessible via alias (for group resolution)
expect(appContainer.resolveByAlias('Logger')[0]).toBeInstanceOf(Logger);
// Accessible via direct key
expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
});
});
import {
bindTo,
Container,
DependencyMissingKeyError,
register,
Registration as R,
scope,
select as s,
singleton,
} from 'ts-ioc-container';
/**
* User Management Domain - Registration Patterns
*
* Registrations define how dependencies are bound to the container.
* Common patterns:
* - Register by class (auto-generates key from class name)
* - Register by value (constants, configuration)
* - Register by factory function (dynamic creation)
* - Register with aliases (multiple keys for same service)
*
* This is the foundation for dependency injection - telling the container
* "when someone asks for X, give them Y".
*/
describe('Registration module', function () {
const createAppContainer = () => new Container({ tags: ['application'] });
it('should register class with scope and lifecycle', function () {
// Logger is registered at application scope as a singleton
@register(bindTo('ILogger'), scope((s) => s.hasTag('application')), singleton())
class Logger {}
const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));
expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
});
it('should register configuration value', function () {
// Register application configuration as a value
const appContainer = createAppContainer().addRegistration(R.fromValue('production').bindToKey('Environment'));
expect(appContainer.resolve('Environment')).toBe('production');
});
it('should register factory function', function () {
// Factory functions are useful for dynamic creation
const appContainer = createAppContainer().addRegistration(
R.fromFn(() => `app-${Date.now()}`).bindToKey('RequestId'),
);
expect(appContainer.resolve('RequestId')).toContain('app-');
});
it('should raise an error if binding key is not provided', () => {
// Values and functions must have explicit keys (classes use class name by default)
expect(() => {
createAppContainer().addRegistration(R.fromValue('orphan-value'));
}).toThrowError(DependencyMissingKeyError);
});
it('should register dependency by class name when no key decorator is used', function () {
// Without @register(bindTo('key')), the class name becomes the key
class FileLogger {}
const appContainer = createAppContainer().addRegistration(R.fromClass(FileLogger));
expect(appContainer.resolve('FileLogger')).toBeInstanceOf(FileLogger);
});
it('should register with multiple keys using aliases', function () {
// Same service accessible via direct key and alias
@register(bindTo('ILogger'), bindTo(s.alias('Logger')), singleton())
class Logger {}
const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));
// Accessible via alias (for group resolution)
expect(appContainer.resolveByAlias('Logger')[0]).toBeInstanceOf(Logger);
// Accessible via direct key
expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
});
});
Fluent API Registration
The fluent API provides more control and is useful when you need dynamic registration or want to avoid decorators.
import {
bindTo,
Container,
DependencyMissingKeyError,
register,
Registration as R,
scope,
select as s,
singleton,
} from 'ts-ioc-container';
/**
* User Management Domain - Registration Patterns
*
* Registrations define how dependencies are bound to the container.
* Common patterns:
* - Register by class (auto-generates key from class name)
* - Register by value (constants, configuration)
* - Register by factory function (dynamic creation)
* - Register with aliases (multiple keys for same service)
*
* This is the foundation for dependency injection - telling the container
* "when someone asks for X, give them Y".
*/
describe('Registration module', function () {
const createAppContainer = () => new Container({ tags: ['application'] });
it('should register class with scope and lifecycle', function () {
// Logger is registered at application scope as a singleton
@register(bindTo('ILogger'), scope((s) => s.hasTag('application')), singleton())
class Logger {}
const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));
expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
});
it('should register configuration value', function () {
// Register application configuration as a value
const appContainer = createAppContainer().addRegistration(R.fromValue('production').bindToKey('Environment'));
expect(appContainer.resolve('Environment')).toBe('production');
});
it('should register factory function', function () {
// Factory functions are useful for dynamic creation
const appContainer = createAppContainer().addRegistration(
R.fromFn(() => `app-${Date.now()}`).bindToKey('RequestId'),
);
expect(appContainer.resolve('RequestId')).toContain('app-');
});
it('should raise an error if binding key is not provided', () => {
// Values and functions must have explicit keys (classes use class name by default)
expect(() => {
createAppContainer().addRegistration(R.fromValue('orphan-value'));
}).toThrowError(DependencyMissingKeyError);
});
it('should register dependency by class name when no key decorator is used', function () {
// Without @register(bindTo('key')), the class name becomes the key
class FileLogger {}
const appContainer = createAppContainer().addRegistration(R.fromClass(FileLogger));
expect(appContainer.resolve('FileLogger')).toBeInstanceOf(FileLogger);
});
it('should register with multiple keys using aliases', function () {
// Same service accessible via direct key and alias
@register(bindTo('ILogger'), bindTo(s.alias('Logger')), singleton())
class Logger {}
const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));
// Accessible via alias (for group resolution)
expect(appContainer.resolveByAlias('Logger')[0]).toBeInstanceOf(Logger);
// Accessible via direct key
expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
});
});
import {
bindTo,
Container,
DependencyMissingKeyError,
register,
Registration as R,
scope,
select as s,
singleton,
} from 'ts-ioc-container';
/**
* User Management Domain - Registration Patterns
*
* Registrations define how dependencies are bound to the container.
* Common patterns:
* - Register by class (auto-generates key from class name)
* - Register by value (constants, configuration)
* - Register by factory function (dynamic creation)
* - Register with aliases (multiple keys for same service)
*
* This is the foundation for dependency injection - telling the container
* "when someone asks for X, give them Y".
*/
describe('Registration module', function () {
const createAppContainer = () => new Container({ tags: ['application'] });
it('should register class with scope and lifecycle', function () {
// Logger is registered at application scope as a singleton
@register(bindTo('ILogger'), scope((s) => s.hasTag('application')), singleton())
class Logger {}
const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));
expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
});
it('should register configuration value', function () {
// Register application configuration as a value
const appContainer = createAppContainer().addRegistration(R.fromValue('production').bindToKey('Environment'));
expect(appContainer.resolve('Environment')).toBe('production');
});
it('should register factory function', function () {
// Factory functions are useful for dynamic creation
const appContainer = createAppContainer().addRegistration(
R.fromFn(() => `app-${Date.now()}`).bindToKey('RequestId'),
);
expect(appContainer.resolve('RequestId')).toContain('app-');
});
it('should raise an error if binding key is not provided', () => {
// Values and functions must have explicit keys (classes use class name by default)
expect(() => {
createAppContainer().addRegistration(R.fromValue('orphan-value'));
}).toThrowError(DependencyMissingKeyError);
});
it('should register dependency by class name when no key decorator is used', function () {
// Without @register(bindTo('key')), the class name becomes the key
class FileLogger {}
const appContainer = createAppContainer().addRegistration(R.fromClass(FileLogger));
expect(appContainer.resolve('FileLogger')).toBeInstanceOf(FileLogger);
});
it('should register with multiple keys using aliases', function () {
// Same service accessible via direct key and alias
@register(bindTo('ILogger'), bindTo(s.alias('Logger')), singleton())
class Logger {}
const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));
// Accessible via alias (for group resolution)
expect(appContainer.resolveByAlias('Logger')[0]).toBeInstanceOf(Logger);
// Accessible via direct key
expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
});
});
Keys
Keys identify dependencies in the container. They can be strings, symbols, or tokens. Understanding how keys work is essential for effective dependency management.
import {
bindTo,
Container,
DependencyMissingKeyError,
register,
Registration as R,
scope,
select as s,
singleton,
} from 'ts-ioc-container';
/**
* User Management Domain - Registration Patterns
*
* Registrations define how dependencies are bound to the container.
* Common patterns:
* - Register by class (auto-generates key from class name)
* - Register by value (constants, configuration)
* - Register by factory function (dynamic creation)
* - Register with aliases (multiple keys for same service)
*
* This is the foundation for dependency injection - telling the container
* "when someone asks for X, give them Y".
*/
describe('Registration module', function () {
const createAppContainer = () => new Container({ tags: ['application'] });
it('should register class with scope and lifecycle', function () {
// Logger is registered at application scope as a singleton
@register(bindTo('ILogger'), scope((s) => s.hasTag('application')), singleton())
class Logger {}
const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));
expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
});
it('should register configuration value', function () {
// Register application configuration as a value
const appContainer = createAppContainer().addRegistration(R.fromValue('production').bindToKey('Environment'));
expect(appContainer.resolve('Environment')).toBe('production');
});
it('should register factory function', function () {
// Factory functions are useful for dynamic creation
const appContainer = createAppContainer().addRegistration(
R.fromFn(() => `app-${Date.now()}`).bindToKey('RequestId'),
);
expect(appContainer.resolve('RequestId')).toContain('app-');
});
it('should raise an error if binding key is not provided', () => {
// Values and functions must have explicit keys (classes use class name by default)
expect(() => {
createAppContainer().addRegistration(R.fromValue('orphan-value'));
}).toThrowError(DependencyMissingKeyError);
});
it('should register dependency by class name when no key decorator is used', function () {
// Without @register(bindTo('key')), the class name becomes the key
class FileLogger {}
const appContainer = createAppContainer().addRegistration(R.fromClass(FileLogger));
expect(appContainer.resolve('FileLogger')).toBeInstanceOf(FileLogger);
});
it('should register with multiple keys using aliases', function () {
// Same service accessible via direct key and alias
@register(bindTo('ILogger'), bindTo(s.alias('Logger')), singleton())
class Logger {}
const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));
// Accessible via alias (for group resolution)
expect(appContainer.resolveByAlias('Logger')[0]).toBeInstanceOf(Logger);
// Accessible via direct key
expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
});
});
import {
bindTo,
Container,
DependencyMissingKeyError,
register,
Registration as R,
scope,
select as s,
singleton,
} from 'ts-ioc-container';
/**
* User Management Domain - Registration Patterns
*
* Registrations define how dependencies are bound to the container.
* Common patterns:
* - Register by class (auto-generates key from class name)
* - Register by value (constants, configuration)
* - Register by factory function (dynamic creation)
* - Register with aliases (multiple keys for same service)
*
* This is the foundation for dependency injection - telling the container
* "when someone asks for X, give them Y".
*/
describe('Registration module', function () {
const createAppContainer = () => new Container({ tags: ['application'] });
it('should register class with scope and lifecycle', function () {
// Logger is registered at application scope as a singleton
@register(bindTo('ILogger'), scope((s) => s.hasTag('application')), singleton())
class Logger {}
const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));
expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
});
it('should register configuration value', function () {
// Register application configuration as a value
const appContainer = createAppContainer().addRegistration(R.fromValue('production').bindToKey('Environment'));
expect(appContainer.resolve('Environment')).toBe('production');
});
it('should register factory function', function () {
// Factory functions are useful for dynamic creation
const appContainer = createAppContainer().addRegistration(
R.fromFn(() => `app-${Date.now()}`).bindToKey('RequestId'),
);
expect(appContainer.resolve('RequestId')).toContain('app-');
});
it('should raise an error if binding key is not provided', () => {
// Values and functions must have explicit keys (classes use class name by default)
expect(() => {
createAppContainer().addRegistration(R.fromValue('orphan-value'));
}).toThrowError(DependencyMissingKeyError);
});
it('should register dependency by class name when no key decorator is used', function () {
// Without @register(bindTo('key')), the class name becomes the key
class FileLogger {}
const appContainer = createAppContainer().addRegistration(R.fromClass(FileLogger));
expect(appContainer.resolve('FileLogger')).toBeInstanceOf(FileLogger);
});
it('should register with multiple keys using aliases', function () {
// Same service accessible via direct key and alias
@register(bindTo('ILogger'), bindTo(s.alias('Logger')), singleton())
class Logger {}
const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));
// Accessible via alias (for group resolution)
expect(appContainer.resolveByAlias('Logger')[0]).toBeInstanceOf(Logger);
// Accessible via direct key
expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
});
});
Scope Registration
Scope match rules (ScopeMatchRule) control which scopes a provider is available in. This allows you to register different implementations for different environments or contexts.
import {
bindTo,
Container,
DependencyMissingKeyError,
register,
Registration as R,
scope,
select as s,
singleton,
} from 'ts-ioc-container';
/**
* User Management Domain - Registration Patterns
*
* Registrations define how dependencies are bound to the container.
* Common patterns:
* - Register by class (auto-generates key from class name)
* - Register by value (constants, configuration)
* - Register by factory function (dynamic creation)
* - Register with aliases (multiple keys for same service)
*
* This is the foundation for dependency injection - telling the container
* "when someone asks for X, give them Y".
*/
describe('Registration module', function () {
const createAppContainer = () => new Container({ tags: ['application'] });
it('should register class with scope and lifecycle', function () {
// Logger is registered at application scope as a singleton
@register(bindTo('ILogger'), scope((s) => s.hasTag('application')), singleton())
class Logger {}
const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));
expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
});
it('should register configuration value', function () {
// Register application configuration as a value
const appContainer = createAppContainer().addRegistration(R.fromValue('production').bindToKey('Environment'));
expect(appContainer.resolve('Environment')).toBe('production');
});
it('should register factory function', function () {
// Factory functions are useful for dynamic creation
const appContainer = createAppContainer().addRegistration(
R.fromFn(() => `app-${Date.now()}`).bindToKey('RequestId'),
);
expect(appContainer.resolve('RequestId')).toContain('app-');
});
it('should raise an error if binding key is not provided', () => {
// Values and functions must have explicit keys (classes use class name by default)
expect(() => {
createAppContainer().addRegistration(R.fromValue('orphan-value'));
}).toThrowError(DependencyMissingKeyError);
});
it('should register dependency by class name when no key decorator is used', function () {
// Without @register(bindTo('key')), the class name becomes the key
class FileLogger {}
const appContainer = createAppContainer().addRegistration(R.fromClass(FileLogger));
expect(appContainer.resolve('FileLogger')).toBeInstanceOf(FileLogger);
});
it('should register with multiple keys using aliases', function () {
// Same service accessible via direct key and alias
@register(bindTo('ILogger'), bindTo(s.alias('Logger')), singleton())
class Logger {}
const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));
// Accessible via alias (for group resolution)
expect(appContainer.resolveByAlias('Logger')[0]).toBeInstanceOf(Logger);
// Accessible via direct key
expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
});
});
import {
bindTo,
Container,
DependencyMissingKeyError,
register,
Registration as R,
scope,
select as s,
singleton,
} from 'ts-ioc-container';
/**
* User Management Domain - Registration Patterns
*
* Registrations define how dependencies are bound to the container.
* Common patterns:
* - Register by class (auto-generates key from class name)
* - Register by value (constants, configuration)
* - Register by factory function (dynamic creation)
* - Register with aliases (multiple keys for same service)
*
* This is the foundation for dependency injection - telling the container
* "when someone asks for X, give them Y".
*/
describe('Registration module', function () {
const createAppContainer = () => new Container({ tags: ['application'] });
it('should register class with scope and lifecycle', function () {
// Logger is registered at application scope as a singleton
@register(bindTo('ILogger'), scope((s) => s.hasTag('application')), singleton())
class Logger {}
const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));
expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
});
it('should register configuration value', function () {
// Register application configuration as a value
const appContainer = createAppContainer().addRegistration(R.fromValue('production').bindToKey('Environment'));
expect(appContainer.resolve('Environment')).toBe('production');
});
it('should register factory function', function () {
// Factory functions are useful for dynamic creation
const appContainer = createAppContainer().addRegistration(
R.fromFn(() => `app-${Date.now()}`).bindToKey('RequestId'),
);
expect(appContainer.resolve('RequestId')).toContain('app-');
});
it('should raise an error if binding key is not provided', () => {
// Values and functions must have explicit keys (classes use class name by default)
expect(() => {
createAppContainer().addRegistration(R.fromValue('orphan-value'));
}).toThrowError(DependencyMissingKeyError);
});
it('should register dependency by class name when no key decorator is used', function () {
// Without @register(bindTo('key')), the class name becomes the key
class FileLogger {}
const appContainer = createAppContainer().addRegistration(R.fromClass(FileLogger));
expect(appContainer.resolve('FileLogger')).toBeInstanceOf(FileLogger);
});
it('should register with multiple keys using aliases', function () {
// Same service accessible via direct key and alias
@register(bindTo('ILogger'), bindTo(s.alias('Logger')), singleton())
class Logger {}
const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));
// Accessible via alias (for group resolution)
expect(appContainer.resolveByAlias('Logger')[0]).toBeInstanceOf(Logger);
// Accessible via direct key
expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
});
});
Registration Pipeline
Registrations support the pipeline pattern, allowing you to chain multiple transformations:
import { args, singleton, scopeAccess } from 'ts-ioc-container';
const container = new Container()
.addRegistration(
R.fromClass(ConfigService)
.bindToKey('IConfig')
.when((s) => s.hasTag('root'))
.pipe(args('/config.json'))
.pipe(singleton())
.pipe(scopeAccess(({ invocationScope }) => invocationScope.hasTag('admin')))
);import { args, singleton, scopeAccess } from 'ts-ioc-container';
const container = new Container()
.addRegistration(
R.fromClass(ConfigService)
.bindToKey('IConfig')
.when((s) => s.hasTag('root'))
.pipe(args('/config.json'))
.pipe(singleton())
.pipe(scopeAccess(({ invocationScope }) => invocationScope.hasTag('admin')))
);Container Modules
Container modules encapsulate registration logic, making it easy to organize and compose your dependency configuration.
import 'reflect-metadata';
import { bindTo, Container, type IContainer, type IContainerModule, register, Registration as R } from 'ts-ioc-container';
/**
* User Management Domain - Container Modules
*
* Modules organize related registrations and allow swapping implementations
* based on environment (development, testing, production).
*
* Common module patterns:
* - ProductionModule: Real database, external APIs, email service
* - DevelopmentModule: In-memory database, mock APIs, console logging
* - TestingModule: Mocks with assertion capabilities
*
* This enables:
* - Easy environment switching
* - Isolated testing without external dependencies
* - Feature flags via module composition
*/
// Auth service interface - same API for all environments
interface IAuthService {
authenticate(email: string, password: string): boolean;
getServiceType(): string;
}
// Production: Real authentication with database lookup
@register(bindTo('IAuthService'))
class ProductionAuthService implements IAuthService {
authenticate(email: string, password: string): boolean {
// In production, this would query the database
return email === 'admin@example.com' && password === 'secure_password';
}
getServiceType(): string {
return 'production';
}
}
// Development: Accepts any credentials for easy testing
@register(bindTo('IAuthService'))
class DevelopmentAuthService implements IAuthService {
authenticate(_email: string, _password: string): boolean {
// Always succeed in development for easier testing
return true;
}
getServiceType(): string {
return 'development';
}
}
// Production module - real services with security
class ProductionModule implements IContainerModule {
applyTo(container: IContainer): void {
container.addRegistration(R.fromClass(ProductionAuthService));
// In a real app, also register:
// - Real database connection
// - External email service
// - Payment gateway
}
}
// Development module - mocks and conveniences
class DevelopmentModule implements IContainerModule {
applyTo(container: IContainer): void {
container.addRegistration(R.fromClass(DevelopmentAuthService));
// In a real app, also register:
// - In-memory database
// - Console email logger
// - Mock payment gateway
}
}
describe('Container Modules', function () {
function createContainer(isProduction: boolean) {
const module = isProduction ? new ProductionModule() : new DevelopmentModule();
return new Container().useModule(module);
}
it('should use production auth with strict validation', function () {
const container = createContainer(true);
const auth = container.resolve<IAuthService>('IAuthService');
expect(auth.getServiceType()).toBe('production');
expect(auth.authenticate('admin@example.com', 'secure_password')).toBe(true);
expect(auth.authenticate('admin@example.com', 'wrong_password')).toBe(false);
});
it('should use development auth with permissive validation', function () {
const container = createContainer(false);
const auth = container.resolve<IAuthService>('IAuthService');
expect(auth.getServiceType()).toBe('development');
// Development mode accepts any credentials
expect(auth.authenticate('any@email.com', 'any_password')).toBe(true);
});
it('should allow composing multiple modules', function () {
// Modules can be composed for feature flags or A/B testing
class FeatureFlagModule implements IContainerModule {
constructor(private enableNewFeature: boolean) {}
applyTo(container: IContainer): void {
if (this.enableNewFeature) {
// Register new feature implementations
}
}
}
const container = new Container().useModule(new ProductionModule()).useModule(new FeatureFlagModule(true));
// Base services from ProductionModule
expect(container.resolve<IAuthService>('IAuthService').getServiceType()).toBe('production');
});
});
import 'reflect-metadata';
import { bindTo, Container, type IContainer, type IContainerModule, register, Registration as R } from 'ts-ioc-container';
/**
* User Management Domain - Container Modules
*
* Modules organize related registrations and allow swapping implementations
* based on environment (development, testing, production).
*
* Common module patterns:
* - ProductionModule: Real database, external APIs, email service
* - DevelopmentModule: In-memory database, mock APIs, console logging
* - TestingModule: Mocks with assertion capabilities
*
* This enables:
* - Easy environment switching
* - Isolated testing without external dependencies
* - Feature flags via module composition
*/
// Auth service interface - same API for all environments
interface IAuthService {
authenticate(email: string, password: string): boolean;
getServiceType(): string;
}
// Production: Real authentication with database lookup
@register(bindTo('IAuthService'))
class ProductionAuthService implements IAuthService {
authenticate(email: string, password: string): boolean {
// In production, this would query the database
return email === 'admin@example.com' && password === 'secure_password';
}
getServiceType(): string {
return 'production';
}
}
// Development: Accepts any credentials for easy testing
@register(bindTo('IAuthService'))
class DevelopmentAuthService implements IAuthService {
authenticate(_email: string, _password: string): boolean {
// Always succeed in development for easier testing
return true;
}
getServiceType(): string {
return 'development';
}
}
// Production module - real services with security
class ProductionModule implements IContainerModule {
applyTo(container: IContainer): void {
container.addRegistration(R.fromClass(ProductionAuthService));
// In a real app, also register:
// - Real database connection
// - External email service
// - Payment gateway
}
}
// Development module - mocks and conveniences
class DevelopmentModule implements IContainerModule {
applyTo(container: IContainer): void {
container.addRegistration(R.fromClass(DevelopmentAuthService));
// In a real app, also register:
// - In-memory database
// - Console email logger
// - Mock payment gateway
}
}
describe('Container Modules', function () {
function createContainer(isProduction: boolean) {
const module = isProduction ? new ProductionModule() : new DevelopmentModule();
return new Container().useModule(module);
}
it('should use production auth with strict validation', function () {
const container = createContainer(true);
const auth = container.resolve<IAuthService>('IAuthService');
expect(auth.getServiceType()).toBe('production');
expect(auth.authenticate('admin@example.com', 'secure_password')).toBe(true);
expect(auth.authenticate('admin@example.com', 'wrong_password')).toBe(false);
});
it('should use development auth with permissive validation', function () {
const container = createContainer(false);
const auth = container.resolve<IAuthService>('IAuthService');
expect(auth.getServiceType()).toBe('development');
// Development mode accepts any credentials
expect(auth.authenticate('any@email.com', 'any_password')).toBe(true);
});
it('should allow composing multiple modules', function () {
// Modules can be composed for feature flags or A/B testing
class FeatureFlagModule implements IContainerModule {
constructor(private enableNewFeature: boolean) {}
applyTo(container: IContainer): void {
if (this.enableNewFeature) {
// Register new feature implementations
}
}
}
const container = new Container().useModule(new ProductionModule()).useModule(new FeatureFlagModule(true));
// Base services from ProductionModule
expect(container.resolve<IAuthService>('IAuthService').getServiceType()).toBe('production');
});
});
Best Practices
- Use decorators for simple cases - When you have straightforward class registrations
- Use fluent API for dynamic registration - When keys or providers are determined at runtime
- Always provide keys for values/factories - Required to avoid errors
- Use modules for organization - Group related registrations together
- Use scope match rules for environment-specific code - Keep configurations clean
- Export tokens as constants - Create keys once and reuse them
- Use aliases for grouping - When you need multiple implementations
Registration Lifecycle
- Creation: Registration is created with a provider
- Configuration: Keys, scopes, and features are configured
- Application: Registration is applied to container via
addRegistration() - Validation: Scope match rules are evaluated
- Storage: Provider is stored in container’s provider map
- Alias Registration: Aliases are registered if provided