@di-framework/di-framework
v3.0.4
Published
Lightweight, zero-dependency TypeScript Dependency Injection framework using decorators. Works seamlessly with SWC and TypeScript's native decorator support.
Maintainers
Readme
di-framework
A lightweight, type-safe Dependency Injection framework for TypeScript using decorators. This framework automatically manages service instantiation, dependency resolution, and lifecycle management.
Installation
No external dependencies required! The framework works with SWC and TypeScript's native decorator support.
Just ensure you have:
- TypeScript 5.0+
- SWC or TypeScript compiler with
experimentalDecoratorsandemitDecoratorMetadataenabled
The decorators are fully integrated with SWC's native support - no need for reflect-metadata or any other polyfill.
Quick Start
1. Basic Service
import { Container } from '@di-framework/di-framework/decorators';
@Container()
export class DatabaseService {
connect(): void {
console.log('Connected to database');
}
}2. Service with Dependencies
import { Container, Component } from '@di-framework/di-framework/decorators';
import { DatabaseService } from './services/DatabaseService';
@Container()
export class UserService {
@Component(DatabaseService)
private db!: DatabaseService;
constructor() {}
getUser(id: string) {
return this.db.query(`SELECT * FROM users WHERE id = '${id}'`);
}
}Note: Property injection is used for all dependencies. This works seamlessly with SWC and TypeScript's native decorator support.
3. Resolve Services
import { useContainer } from '@di-framework/di-framework/container';
import { UserService } from './services/UserService';
const container = useContainer();
const userService = container.resolve<UserService>(UserService);
// All dependencies are automatically injected!
userService.getUser('123');API Reference
@Container(options?)
Marks a class as injectable and automatically registers it with the DI container.
Options:
singleton?: boolean(default:true) - Create a new instance each time or reuse the same instancecontainer?: DIContainer- Specify a custom container (defaults to global container)- Note: Import as
import { Container as DIContainer } from '@di-framework/di-framework/container'to avoid name collision with the@Containerdecorator.
- Note: Import as
Example:
@Container({ singleton: false })
export class RequestScopedService {
// New instance created for each resolution
}@Component(target)
Marks a constructor parameter or property for dependency injection.
Parameters:
target- The class to inject or a string identifier for factory-registered services
Example - Constructor Parameter:
@Container()
export class OrderService {
constructor(@Component(DatabaseService) private db: DatabaseService) {}
}Example - Property Injection:
@Container()
export class ReportService {
@Component(DatabaseService)
private db: DatabaseService;
}@Telemetry(options?)
Marks a method for telemetry tracking. When called, it emits a telemetry event on the container. Works with both synchronous and asynchronous methods.
Options:
logging?: boolean(default:false) - If true, logs the method execution details (status and duration) to the console.
Example:
@Container()
export class ApiService {
@Telemetry({ logging: true })
async fetchData(id: string) {
// ...
}
}@TelemetryListener()
Marks a method as a listener for telemetry events. The method will be automatically registered to the container's telemetry event when the service is instantiated.
Example:
@Container()
export class MonitoringService {
@TelemetryListener()
onTelemetry(event: any) {
console.log(
`Method ${event.className}.${event.methodName} took ${event.endTime - event.startTime}ms`,
);
}
}useContainer()
Returns the global DI container instance.
import { useContainer } from '@di-framework/di-framework/container';
const container = useContainer();container.register(serviceClass, options?)
Manually register a service class.
container.register(UserService, { singleton: true });container.registerFactory(name, factory, options?)
Register a service using a factory function.
container.registerFactory(
'config',
() => ({
apiKey: process.env.API_KEY,
dbUrl: process.env.DATABASE_URL,
}),
{ singleton: true },
);container.resolve(serviceClass)
Resolve and get an instance of a service.
const userService = container.resolve<UserService>(UserService);
// or by name
const config = container.resolve('config');container.has(serviceClass)
Check if a service is registered.
if (container.has(UserService)) {
const service = container.resolve(UserService);
}container.getServiceNames()
Get all registered service names.
const names = container.getServiceNames();
console.log(names); // ['DatabaseService', 'UserService', ...]container.on(event, listener)
Subscribe to DI container lifecycle events (observer pattern).
Events:
registered- fired when a class or factory is registeredresolved- fired whenever a service is resolved (cached or fresh)constructed- fired whenconstruct()creates a new instancecleared- fired when the container is cleared
Example:
const unsubscribe = container.on('resolved', ({ key, fromCache }) => {
console.log(`Resolved ${typeof key === 'string' ? key : key.name} (fromCache=${fromCache})`);
});
unsubscribe(); // stop listeningcontainer.construct(serviceClass, overrides?)
Create a fresh instance without registering it, while still honoring dependency injection. Useful for constructor-pattern scenarios where you need to supply specific primitives/config values.
import { Component } from '@di-framework/di-framework/decorators';
import { LoggerService } from '@di-framework/di-framework/services/LoggerService';
class Greeter {
constructor(
@Component(LoggerService) private logger: LoggerService,
private greeting: string,
) {}
}
const greeter = container.construct(Greeter, { 1: 'hello world' });container.fork(options?)
Clone the container registrations (prototype pattern) into a new container. Pass { carrySingletons: true } to reuse existing singleton instances; default is to start with fresh instances.
const testContainer = container.fork({ carrySingletons: false });Advanced Examples
Multiple Dependencies
@Container()
export class ApplicationContext {
constructor(
@Component(DatabaseService) private db: DatabaseService,
@Component(LoggerService) private logger: LoggerService,
@Component(AuthService) private auth: AuthService,
) {}
async initialize() {
this.logger.log('Initializing application...');
await this.db.connect();
this.auth.setup();
}
}Transient (Non-Singleton) Services
@Container({ singleton: false })
export class RequestContext {
id = Math.random().toString();
constructor(@Component(LoggerService) private logger: LoggerService) {
this.logger.log(`Request context created: ${this.id}`);
}
}
// Each resolve creates a new instance
const ctx1 = container.resolve(RequestContext); // new instance
const ctx2 = container.resolve(RequestContext); // different instanceLifecycle Methods
Services can optionally implement lifecycle methods:
@Container()
export class DatabaseService {
private connected = false;
setEnv(env: Record<string, any>) {
// Called to initialize environment-specific config
console.log('DB URL:', env.DATABASE_URL);
}
setCtx(context: any) {
// Called to set execution context
console.log('Context:', context);
}
connect() {
this.connected = true;
}
}
// Calling lifecycle methods
const db = container.resolve(DatabaseService);
db.setEnv(process.env);
db.setCtx({ userId: '123' });
db.connect();Factory Functions
container.registerFactory(
'apiClient',
() => {
return new HttpClient({
baseUrl: process.env.API_URL,
timeout: 5000,
});
},
{ singleton: true },
);
// Use in services
@Container()
export class UserService {
constructor(@Component('apiClient') private api: any) {}
}How It Works
- Decoration: When you decorate a class with
@Container(), the decorator registers it with the global container - Registration: The class is stored in the container with metadata about its dependencies
- Resolution: When you call
container.resolve(ServiceClass):- The container creates a new instance (or returns existing singleton)
- It examines the constructor parameters and their types
- It recursively resolves each dependency
- Dependencies are injected into the constructor
- The configured instance is returned
- Caching: Singleton instances are cached and reused
Comparison with SAMPLE.ts
Before (Manual - SAMPLE.ts)
const createServerContext = (env, ctx) => {
if (!instanceState.member) {
const contextInstance = Context.create({
contactService: ContactService.create({}),
assetService: AssetService.create({}),
transactionService: TransactionService.create({}),
// ... 20+ more services manually created and wired
chatService: ChatService.create({
openAIApiKey: env.OPENAI_API_KEY,
// ... manual configuration
}),
});
instanceState.member = contextInstance;
}
instanceState.member.setEnv(env);
instanceState.member.setCtx(ctx);
// ... manual dependency wiring
instanceState.member.knowledgeService.setAttachmentService(
instanceState.member.attachmentService,
);
return instanceState.member;
};After (DI Framework)
@Container()
export class ApplicationContext {
constructor(
@Component(ContactService) private contactService: ContactService,
@Component(AssetService) private assetService: AssetService,
@Component(TransactionService)
private transactionService: TransactionService,
// ... all services automatically injected
@Component(ChatService) private chatService: ChatService,
) {}
setEnv(env: Record<string, any>) {
// Services are already available via constructor injection
this.chatService.initialize(env.OPENAI_API_KEY);
}
setCtx(ctx: any) {
// All services have access to context
}
}
// Usage
const container = useContainer();
const appContext = container.resolve(ApplicationContext);
appContext.setEnv(env);
appContext.setCtx(ctx);Benefits:
- No manual service instantiation
- No manual dependency wiring
- Automatic singleton management
- Type-safe dependency resolution
- Easier to test (mock services simply by registering test implementations)
- Scales better as services grow
Error Handling
Circular Dependencies
// This will be detected and throw an error:
@Container()
class ServiceA {
constructor(@Component(ServiceB) private b: ServiceB) {}
}
@Container()
class ServiceB {
constructor(@Component(ServiceA) private a: ServiceA) {}
}
// Error: Circular dependency detected while resolving ServiceAUnregistered Services
@Container()
class MyService {
constructor(@Component(UnregisteredService) private s: UnregisteredService) {}
}
// Error: Service 'UnregisteredService' is not registered in the DI containerBest Practices
- Mark all services with
@Container()- Makes dependency management explicit - Use constructor injection - Preferred over property injection for mandatory dependencies
- Use property injection for optional dependencies - Keep it minimal
- No need to import reflect-metadata - This framework uses a lightweight metadata store
- Separate service interfaces from implementations - For easier testing
- Use singletons for stateless services - Most services should be singletons
- Use transient (non-singleton) for stateful services - Request/session scoped services
Testing
// Create a test container
import { Container as DIContainer } from '@di-framework/di-framework/container';
const testContainer = new DIContainer();
// Register mock implementations
class MockDatabaseService {
query() {
return { mock: true };
}
}
testContainer.register(MockDatabaseService);
// Register dependencies
testContainer.register(UserService);
// Test the service with mocked dependencies
const userService = testContainer.resolve(UserService);
expect(userService.getUser('1')).toEqual({ mock: true });License
MIT
