injectkit
v1.0.3
Published
Lightweight, type-safe dependency injection container for TypeScript with constructor injection, factory functions, and lifetime management.
Downloads
315
Maintainers
Readme
InjectKit
Features
- 🎯 Type-safe — Full TypeScript support with strong typing throughout
- 🪶 Lightweight — Minimal footprint with zero dependencies (except
reflect-metadata) - 🔄 Multiple lifetimes — Singleton, transient, and scoped instance management
- 🏭 Flexible registration — Classes, factories, or existing instances
- 📦 Collection support — Register arrays and maps of implementations
- 🔍 Validation — Automatic detection of missing and circular dependencies
- 🧪 Test-friendly — Easy mocking with scoped container overrides
Installation
npm install injectkit reflect-metadatapnpm add injectkit reflect-metadatayarn add injectkit reflect-metadataRequirements
- Node.js >= 20
- TypeScript with the following compiler options enabled:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}- Import
reflect-metadataonce at your application entry point:
import 'reflect-metadata';Quick Start
import 'reflect-metadata';
import { Injectable, InjectKitRegistry, Container } from 'injectkit';
// 1. Decorate your classes with @Injectable()
@Injectable()
class Logger {
log(message: string) {
console.log(`[LOG] ${message}`);
}
}
@Injectable()
class UserService {
constructor(private logger: Logger) {}
createUser(name: string) {
this.logger.log(`Creating user: ${name}`);
return { id: crypto.randomUUID(), name };
}
}
// 2. Create a registry and register your services
const registry = new InjectKitRegistry();
registry.register(Logger).useClass(Logger).asSingleton();
registry.register(UserService).useClass(UserService).asSingleton();
// 3. Build the container
const container = registry.build();
// 4. Resolve and use your services
const userService = container.get(UserService);
userService.createUser('Alice');Core Concepts
Registry
The Registry is where you configure your services before runtime. It validates all registrations when building the container.
const registry = new InjectKitRegistry();
// Register services
registry.register(MyService).useClass(MyService).asSingleton();
// Check if registered
registry.isRegistered(MyService); // true
// Remove if needed
registry.remove(MyService);
// Build the container when ready
const container = registry.build();Container
The Container resolves and manages service instances at runtime. It automatically injects dependencies declared in constructors.
// Resolve a service (dependencies are injected automatically)
const service = container.get(MyService);
// The Container itself can be resolved for factory patterns
const container = container.get(Container);Identifier
An Identifier is a class constructor or abstract class used to register and resolve services. This enables programming to interfaces:
// Abstract class as identifier
abstract class Repository {
abstract find(id: string): Promise<Entity>;
}
// Concrete implementation
@Injectable()
class PostgresRepository extends Repository {
async find(id: string) {
/* ... */
}
}
// Register abstract → concrete mapping
registry.register(Repository).useClass(PostgresRepository).asSingleton();
// Resolve using the abstract class
const repo = container.get(Repository); // Returns PostgresRepositoryLifetimes
InjectKit supports three lifetime strategies:
| Lifetime | Behavior |
| ------------- | ------------------------------------------------- |
| Singleton | One instance shared across the entire application |
| Transient | New instance created on every get() call |
| Scoped | One instance per scope, shared within that scope |
registry.register(ConfigService).useClass(ConfigService).asSingleton();
registry.register(RequestId).useClass(RequestId).asScoped();
registry.register(TempCalculation).useClass(TempCalculation).asTransient();API Reference
Registration Methods
useClass(constructor)
Register a service using its constructor. Dependencies are automatically resolved from constructor parameters.
@Injectable()
class EmailService {
constructor(
private config: ConfigService,
private logger: Logger,
) {}
}
registry.register(EmailService).useClass(EmailService).asSingleton();useFactory(factory)
Register a service using a factory function. Useful for complex initialization or third-party libraries.
registry
.register(DatabaseConnection)
.useFactory(container => {
const config = container.get(ConfigService);
return new DatabaseConnection({
host: config.dbHost,
port: config.dbPort,
});
})
.asSingleton();useInstance(instance)
Register an existing instance directly. Always behaves as a singleton.
const config = new ConfigService({ env: 'production' });
registry.register(ConfigService).useInstance(config);useArray(constructor)
Register a collection of implementations. Useful for plugin systems or strategy patterns.
// Handler implementations
@Injectable()
class JsonHandler extends Handler {
/* ... */
}
@Injectable()
class XmlHandler extends Handler {
/* ... */
}
// Array container
@Injectable()
class Handlers extends Array<Handler> {}
// Registration
registry.register(JsonHandler).useClass(JsonHandler).asSingleton();
registry.register(XmlHandler).useClass(XmlHandler).asSingleton();
registry.register(Handlers).useArray(Handlers).push(JsonHandler).push(XmlHandler);
// Usage
const handlers = container.get(Handlers);
handlers.forEach(h => h.handle(data));useMap(constructor)
Register a keyed collection of implementations.
@Injectable()
class ProcessorMap extends Map<string, Processor> {}
registry.register(ProcessorMap).useMap(ProcessorMap).set('fast', FastProcessor).set('accurate', AccurateProcessor);
// Usage
const processors = container.get(ProcessorMap);
const processor = processors.get('fast');Container Methods
get<T>(identifier): T
Resolves an instance of the specified type.
const service = container.get(MyService);createScopedContainer(): ScopedContainer
Creates a child container for scoped instance management.
const requestScope = container.createScopedContainer();
const requestService = requestScope.get(RequestScopedService);override<T>(identifier, instance): void
Overrides a registration within a scoped container. Perfect for testing.
const testScope = container.createScopedContainer();
// Override with a mock
testScope.override(EmailService, {
send: vi.fn().mockResolvedValue(true),
} as EmailService);
// Tests use the mock
const service = testScope.get(NotificationService);Registry Methods
register<T>(identifier): RegistrationType<T>
Starts a registration chain for a service.
remove<T>(identifier): void
Removes a registration from the registry.
isRegistered<T>(identifier): boolean
Checks if a service is already registered.
build(): Container
Builds the container, validating all registrations.
Scoped Containers
Scoped containers enable request-scoped or unit-of-work patterns:
@Injectable()
class RequestContext {
readonly requestId = crypto.randomUUID();
readonly startTime = Date.now();
}
registry.register(RequestContext).useClass(RequestContext).asScoped();
// Per-request handling
app.use((req, res, next) => {
const scope = container.createScopedContainer();
// Same RequestContext instance throughout this request
const ctx = scope.get(RequestContext);
req.scope = scope;
next();
});Scope Hierarchy
Scoped containers inherit instances from parent scopes:
const root = registry.build();
const scope1 = root.createScopedContainer();
const scope2 = scope1.createScopedContainer();
// Instance created in scope1 is visible in scope2
const instance1 = scope1.get(ScopedService);
const instance2 = scope2.get(ScopedService);
console.log(instance1 === instance2); // trueValidation
InjectKit validates your dependency graph when calling build():
Missing Dependencies
@Injectable()
class UserService {
constructor(private db: DatabaseService) {} // Not registered!
}
registry.register(UserService).useClass(UserService).asSingleton();
registry.build(); // ❌ Error: Missing dependencies for UserService: DatabaseServiceCircular Dependencies
@Injectable()
class ServiceA {
constructor(private b: ServiceB) {}
}
@Injectable()
class ServiceB {
constructor(private a: ServiceA) {}
}
registry.register(ServiceA).useClass(ServiceA).asSingleton();
registry.register(ServiceB).useClass(ServiceB).asSingleton();
registry.build(); // ❌ Error: Circular dependency found: ServiceA -> ServiceB -> ServiceAMissing Decorator
class ForgotDecorator {
constructor(private dep: SomeDependency) {}
}
registry.register(ForgotDecorator).useClass(ForgotDecorator).asSingleton();
registry.build(); // ❌ Error: Service not decorated: ForgotDecoratorTesting
InjectKit makes testing easy with scoped overrides:
import { describe, it, expect, beforeEach } from 'vitest';
describe('UserService', () => {
let scope: ScopedContainer;
let mockDb: DatabaseService;
beforeEach(() => {
scope = container.createScopedContainer();
mockDb = {
query: vi.fn().mockResolvedValue([{ id: '1', name: 'Test' }]),
} as unknown as DatabaseService;
scope.override(DatabaseService, mockDb);
});
it('should fetch users', async () => {
const userService = scope.get(UserService);
const users = await userService.getUsers();
expect(users).toHaveLength(1);
expect(mockDb.query).toHaveBeenCalled();
});
});TypeScript Configuration
Recommended tsconfig.json settings:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strict": true
}
}License
MIT
