β οΈ Beta Release Notice
Kizuna is currently in beta. While the core functionality is stable and production use can be considered, there may be small API changes and improvements based on community feedback. We recommend thorough testing before production deployment.
A lightweight, type-safe dependency injection container for TypeScript and JavaScript applications. Kizuna provides a unified, intuitive API for managing service lifecycles with comprehensive type safety and IDE autocompletion.
- π― Comprehensive Type Safety: Full TypeScript support with automatic type inference
- π Unified API: Single API supporting all registration patterns with a focus on developer experience
- π Multiple Lifecycles: Singleton, Scoped, and Transient service management
- π Flexible Registration: Constructor, interface, and factory-based service registration
- π‘οΈ Parameter Validation: Automatic validation of dependency names vs constructor parameters
- π Enhanced IDE Support: Full autocompletion and compile-time validation
- β‘ Zero Dependencies: Lightweight with no external dependencies
- π Cross-Platform: Works in Node.js, browsers, and edge environments
npm install @shirudo/kizunaimport { ContainerBuilder } from '@shirudo/kizuna';
// Define your services
class Logger {
log(message: string) { console.log(`[LOG] ${message}`); }
}
class DatabaseService {
constructor(private logger: Logger) {}
connect() { this.logger.log('Connected to database'); }
}
class UserService {
constructor(private db: DatabaseService, private logger: Logger) {}
getUser(id: string) {
this.db.connect();
this.logger.log(`Getting user ${id}`);
return { id, name: 'John Doe' };
}
}
// π― Register services with full type safety
const container = new ContainerBuilder()
.registerSingleton('Logger', Logger) // Type: Logger β¨
.registerSingleton('Database', DatabaseService, 'Logger') // Dependencies as strings
.registerScoped('UserService', UserService, 'Database', 'Logger')
.build();
// β
Get services with enhanced IDE autocompletion
const userService = container.get('UserService'); // Type: UserService (auto-inferred!)
const user = userService.getUser('123'); // Full IntelliSense supportKizuna provides a single, comprehensive API that combines type safety and flexibility. All registration patterns work together with full type inference.
For services with constructor dependencies:
const container = new ContainerBuilder()
.registerSingleton('Config', ConfigService)
.registerScoped('UserService', UserService, 'Config') // Dependencies as strings
.registerTransient('EmailService', EmailService, 'Config')
.build();
// IDE suggests: 'Config', 'UserService', 'EmailService'
const userService = container.get('UserService'); // Type: UserService β¨For implementing abstractions and polymorphism:
interface IEmailService {
send(to: string, subject: string, body: string): Promise<void>;
}
class SMTPEmailService implements IEmailService {
async send(to: string, subject: string, body: string) { /* implementation */ }
}
const container = new ContainerBuilder()
.registerSingleton('Logger', Logger)
.registerSingletonInterface<IEmailService>('EmailService', SMTPEmailService, 'Logger')
.registerScopedInterface<ICache>('Cache', RedisCache, 'Logger')
.build();
const emailService = container.get('EmailService'); // Type: IEmailService β¨For complex initialization, conditional logic, or primitive values:
const container = new ContainerBuilder()
.registerSingleton('Logger', Logger)
// Factory returning objects
.registerSingletonFactory('Config', (provider) => {
const logger = provider.get('Logger'); // Type: Logger β¨
logger.log('Loading configuration...');
return {
environment: process.env.NODE_ENV || 'development',
database: { url: 'postgresql://localhost:5432/app' },
features: { analytics: true }
};
})
// Factory returning primitives
.registerSingletonFactory('MaxRetries', () => 3)
.registerSingletonFactory('SupportedLanguages', () => ['en', 'es', 'fr', 'de'])
// Factory returning functions
.registerSingletonFactory('Validator', () => ({
email: (value: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
required: (value: any) => value != null && value !== ''
}))
.build();
const config = container.get('Config'); // Type: inferred from factory return! β¨
const maxRetries = container.get('MaxRetries'); // Type: number β¨
const validator = container.get('Validator'); // Type: validation functions object β¨Every registration pattern supports all three lifecycles:
const container = new ContainerBuilder()
// Singleton services (shared across entire application)
.registerSingleton('Config', ConfigService)
.registerSingletonInterface<ILogger>('Logger', ConsoleLogger)
.registerSingletonFactory('Database', (provider) => createConnection())
// Scoped services (shared within scope, new per scope)
.registerScoped('RequestContext', RequestContext, 'Logger')
.registerScopedInterface<ICache>('Cache', MemoryCache, 'Logger')
.registerScopedFactory('RequestId', () => crypto.randomUUID())
// Transient services (new instance every time)
.registerTransient('EmailService', EmailService, 'Logger')
.registerTransientInterface<IValidator>('Validator', DefaultValidator)
.registerTransientFactory('Timestamp', () => Date.now())
.build();Kizuna provides compile-time type checking and IDE integration:
const container = new ContainerBuilder()
.registerSingleton('UserService', UserService, 'Logger')
.build();
// β TypeScript Error: 'NonExistent' doesn't exist in registry
const invalid = container.get('NonExistent');
// Autocompletion suggests only registered services
const service = container.get(''); // IDE suggests: 'UserService'const builder = new ContainerBuilder()
.registerSingleton('Service', SomeService, 'MissingDependency'); // Oops!
// Catch configuration errors before runtime
const issues = builder.validate();
// Returns: ["Service depends on unregistered service 'MissingDependency'"]
if (issues.length === 0) {
const container = builder.build();
} else {
console.error('Configuration issues:', issues);
}Kizuna automatically validates that your dependency names match constructor parameter names, preventing runtime errors from incorrect dependency order:
class EmailService {
// Constructor parameters: logger, mailer
constructor(private logger: Logger, private mailer: MailService) {}
}
// β Wrong parameter order - will fail validation
const builder = new ContainerBuilder()
.registerSingleton('Logger', Logger)
.registerSingleton('MailService', MailService, 'Logger')
.registerScoped('EmailService', EmailService, 'MailService', 'Logger'); // Wrong order!
const issues = builder.validate();
// Returns: [
// "Service 'EmailService' parameter 0 is named 'logger' but dependency 'MailService' is provided",
// "Service 'EmailService' parameter 1 is named 'mailer' but dependency 'Logger' is provided"
// ]
// β
Correct parameter order - validation passes
const correctBuilder = new ContainerBuilder()
.registerSingleton('Logger', Logger)
.registerSingleton('MailService', MailService, 'Logger')
.registerScoped('EmailService', EmailService, 'logger', 'mailer'); // Matches constructor!
correctBuilder.validate(); // Returns: [] (no issues)Benefits:
- Helps Prevent Runtime Errors: Catches dependency order mismatches at validation time
- Enabled by Default: Works automatically with no setup required
- Helpful Suggestions: Provides corrected registration examples in error messages
- Opt-out Available: Can be disabled if needed with
.disableStrictParameterValidation()
When Parameter Validation Helps:
// Before: Runtime error when EmailService tries to use dependencies
class EmailService {
constructor(private logger: Logger, private config: ConfigService) {}
sendEmail() {
this.logger.log('Sending email...'); // π₯ Runtime error if dependencies swapped!
}
}
// After: Validation catches the error before runtime
builder.validate(); // Catches parameter name mismatches earlyDisable if needed (not recommended):
const container = new ContainerBuilder()
.disableStrictParameterValidation() // Turn off validation
.registerScoped('EmailService', EmailService, 'config', 'logger') // Order doesn't matter
.build();Scopes provide service isolation for request processing, transactions, and multi-tenant scenarios:
const container = new ContainerBuilder()
.registerSingleton('Logger', Logger) // Shared across all requests
.registerScoped('RequestContext', RequestContext) // Unique per request
.registerScoped('UserService', UserService, 'Logger', 'RequestContext')
.build();
// Express.js middleware
app.use((req, res, next) => {
req.scope = container.startScope(); // Create request scope
res.on('finish', () => req.scope.dispose()); // Cleanup when done
next();
});
app.get('/users/:id', (req, res) => {
const userService = req.scope.get('UserService'); // Request-specific instance
const user = userService.getUser(req.params.id);
res.json(user);
});async function withTransaction<T>(work: (scope: ServiceLocator) => Promise<T>): Promise<T> {
const transactionScope = container.startScope();
try {
// Register transaction-specific connection
const connection = await createConnection();
transactionScope.registerInstance('Connection', connection);
await connection.beginTransaction();
const result = await work(transactionScope);
await connection.commit();
return result;
} catch (error) {
await connection.rollback();
throw error;
} finally {
transactionScope.dispose(); // Always cleanup
}
}
// Usage
await withTransaction(async (txScope) => {
const userRepo = txScope.get('UserRepository'); // Uses transaction connection
const orderRepo = txScope.get('OrderRepository'); // Same transaction
const user = await userRepo.create({ name: 'John' });
await orderRepo.create({ userId: user.id, total: 100 });
});For complex applications, separate containers maintain domain boundaries:
// Shared infrastructure
const sharedContainer = new ContainerBuilder()
.registerSingleton('Logger', Logger)
.registerSingleton('EmailService', EmailService, 'Logger')
.registerSingletonInterface<IConfig>('Config', DatabaseConfig)
.build();
// User domain container
const userContainer = new ContainerBuilder()
.registerSingletonFactory('Logger', () => sharedContainer.get('Logger'))
.registerSingletonFactory('EmailService', () => sharedContainer.get('EmailService'))
.registerScoped('UserService', UserService, 'Logger')
.registerScoped('UserNotificationService', UserNotificationService, 'EmailService')
.build();
// Order domain container
const orderContainer = new ContainerBuilder()
.registerSingletonFactory('Logger', () => sharedContainer.get('Logger'))
.registerScoped('OrderService', OrderService, 'Logger')
.registerScoped('PaymentService', PaymentService, 'Logger')
.build();
// Each domain has isolated services but shares infrastructure
const userService = userContainer.startScope().get('UserService');
const orderService = orderContainer.startScope().get('OrderService');describe('UserService', () => {
let testContainer: TypeSafeServiceLocator<any>;
beforeEach(() => {
testContainer = new ContainerBuilder()
.registerSingletonFactory('Logger', () => ({
log: jest.fn(),
error: jest.fn()
} as any))
.registerSingletonFactory('Database', () => mockDatabase)
.registerScoped('UserService', UserService, 'Database', 'Logger')
.build();
});
it('should create user with mocked dependencies', async () => {
const userService = testContainer.get('UserService'); // Type: UserService β¨
const user = await userService.createUser({ name: 'Test User' });
expect(user.id).toBeDefined();
});
});const container = new ContainerBuilder()
.registerSingletonFactory('Config', () => ({
environment: process.env.NODE_ENV || 'development',
database: { url: process.env.DATABASE_URL },
redis: { url: process.env.REDIS_URL }
}))
.registerSingletonFactory('EmailService', (provider) => {
const config = provider.get('Config');
// Environment-specific implementations
return config.environment === 'production'
? new SMTPEmailService(config.smtp)
: new MockEmailService();
})
.registerSingletonFactory('Cache', (provider) => {
const config = provider.get('Config');
return config.redis.url
? new RedisCache(config.redis.url)
: new InMemoryCache();
})
.build();Check out comprehensive examples in the examples/ directory:
unified-container-example.ts- Complete unified API demonstrationmultiple-containers-domain-separation.ts- E-commerce app with domain separationvalidation-example.ts- Configuration validation patterns
The main class for configuring your dependency injection container.
// Singleton lifecycle
.registerSingleton<K, T>(key: K, serviceType: new (...args: any[]) => T, ...dependencies: string[])
.registerSingletonInterface<T, K>(key: K, implementationType: new (...args: any[]) => T, ...dependencies: string[])
.registerSingletonFactory<K, T>(key: K, factory: (provider: TypeSafeServiceLocator<TRegistry>) => T)
// Scoped lifecycle (one instance per scope)
.registerScoped<K, T>(key: K, serviceType: new (...args: any[]) => T, ...dependencies: string[])
.registerScopedInterface<T, K>(key: K, implementationType: new (...args: any[]) => T, ...dependencies: string[])
.registerScopedFactory<K, T>(key: K, factory: (provider: TypeSafeServiceLocator<TRegistry>) => T)
// Transient lifecycle (new instance every time)
.registerTransient<K, T>(key: K, serviceType: new (...args: any[]) => T, ...dependencies: string[])
.registerTransientInterface<T, K>(key: K, implementationType: new (...args: any[]) => T, ...dependencies: string[])
.registerTransientFactory<K, T>(key: K, factory: (provider: TypeSafeServiceLocator<TRegistry>) => T).build(): TypeSafeServiceLocator<TRegistry> // Build the container
.validate(): string[] // Validate configuration
.clear(): ContainerBuilder // Clear all registrations
.disableStrictParameterValidation(): ContainerBuilder // Disable parameter name validation
.count: number // Number of registered services
.isRegistered(key: string): boolean // Check if service is registeredThe built container interface for service resolution.
interface TypeSafeServiceLocator<TRegistry> {
get<K extends keyof TRegistry>(key: K): TRegistry[K]; // Resolve service
startScope(): TypeSafeServiceLocator<TRegistry>; // Create new scope
dispose(): void; // Cleanup resources
}- Singleton: One instance per container (application lifetime)
- Scoped: One instance per scope (request/transaction lifetime)
- Transient: New instance every time requested
Kizuna works across different JavaScript environments:
- Node.js: Version 18.0.0 and above
- Browsers: Modern browsers supporting ES2020+
- Edge Environments: Cloudflare Workers, Vercel Edge Functions, etc.
- Other Runtimes: Deno, Bun, and other JavaScript runtimes
Important: Kizuna is optimized for JavaScript's single-threaded model and is not thread-safe. For concurrent environments:
// Container-per-worker (recommended)
const worker = new Worker('worker.js');
// Each worker creates its own container
// Request-scoped isolation (web servers)
app.use((req, res, next) => {
req.services = rootContainer.startScope(); // Isolated per request
res.on('finish', () => req.services.dispose());
});// DON'T share containers across threads
const sharedContainer = builder.build();
worker1.postMessage({ container: sharedContainer }); // β Race conditions
worker2.postMessage({ container: sharedContainer }); // β Unsafeπ For detailed guidance, see our Concurrency Patterns Guide
Kizuna is built with TypeScript and provides comprehensive type safety. Ensure your tsconfig.json includes:
{
"compilerOptions": {
"moduleResolution": "node",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true
}
}Contributions are welcome! Please feel free to submit a Pull Request.
MIT - see LICENSE file for details.
This project was inspired by the foundational work of Remi Henache on the injected-ts library.
Kizuna (η΅) - Creating strong bonds between your application's services through dependency injection. π€