
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
@navios/di
Advanced tools
A powerful, type-safe dependency injection library for TypeScript applications. Navios DI provides a modern, decorator-based approach to dependency injection with support for singletons, transients, request-scoped services, factories, injection tokens, and service lifecycle management.
npm install @navios/di
# or
yarn add @navios/di
import { asyncInject, Container, Injectable } from '@navios/di'
@Injectable()
class DatabaseService {
async connect() {
return 'Connected to database'
}
}
@Injectable()
class UserService {
private readonly db = asyncInject(DatabaseService)
async getUsers() {
const dbService = await this.db
const connection = await dbService.connect()
return `Users from ${connection}`
}
}
// Using Container
const container = new Container()
const userService = await container.get(UserService)
console.log(await userService.getUsers()) // "Users from Connected to database"
The Container class provides a simplified API for dependency injection:
import { Container } from '@navios/di'
const container = new Container()
// Get instances
const service = await container.get(MyService)
// Invalidate services and their dependencies
await container.invalidate(service)
// Wait for all pending operations
await container.ready()
// Clean up all resources
await container.dispose()
For request-scoped services, use ScopedContainer which provides isolated service resolution:
import { Container, Injectable, InjectableScope } from '@navios/di'
@Injectable({ scope: InjectableScope.Request })
class RequestLogger {
constructor() {
console.log('New logger for this request')
}
}
const container = new Container()
// Begin a request context - returns a ScopedContainer
const scopedContainer = container.beginRequest('req-123', { userId: 456 })
// Use the scoped container for request-scoped services
const logger = await scopedContainer.get(RequestLogger)
// Access metadata
scopedContainer.setMetadata('correlationId', 'abc-123')
const corrId = scopedContainer.getMetadata('correlationId')
// End the request (cleanup all request-scoped instances)
await scopedContainer.endRequest()
The @Injectable decorator marks a class as injectable:
import { Injectable, InjectableScope } from '@navios/di'
import { z } from 'zod'
// Singleton (default)
@Injectable()
class SingletonService {}
// Transient (new instance each time)
@Injectable({ scope: InjectableScope.Transient })
class TransientService {}
// Request-scoped (new instance per request context)
@Injectable({ scope: InjectableScope.Request })
class RequestService {}
// With custom injection token
@Injectable({ token: MyToken })
class TokenizedService {}
// With priority (higher priority wins when multiple registrations exist)
@Injectable({ priority: 100 })
class DefaultService {}
@Injectable({ priority: 200 }) // This wins
class OverrideService {}
// With schema for constructor arguments
const configSchema = z.object({
host: z.string(),
port: z.number(),
})
@Injectable({ schema: configSchema })
class DatabaseConfig {
constructor(public readonly config: z.output<typeof configSchema>) {}
}
inject - Synchronous InjectionUse inject for immediate access to dependencies. Note: If the dependency is not immediately available, inject returns a proxy that will throw an error if accessed before the dependency is ready:
@Injectable()
class EmailService {
sendEmail(message: string) {
return `Email sent: ${message}`
}
}
@Injectable()
class NotificationService {
private readonly emailService = inject(EmailService)
notify(message: string) {
// Safe to use if EmailService is already instantiated
return this.emailService.sendEmail(message)
}
}
asyncInject - Asynchronous InjectionUse asyncInject for async dependency resolution, especially useful for circular dependencies:
@Injectable()
class AsyncService {
private readonly emailService = asyncInject(EmailService)
async notify(message: string) {
const emailService = await this.emailService
return emailService.sendEmail(message)
}
}
optional - Optional InjectionUse optional to inject a dependency only if it's available:
@Injectable()
class FeatureService {
private readonly analytics = optional(AnalyticsService)
track(event: string) {
// Only calls analytics if the service is available
this.analytics?.track(event)
}
}
Create instances using factory classes:
import { Factory, Factorable, FactoryContext } from '@navios/di'
@Factory()
class DatabaseConnectionFactory implements Factorable<Connection> {
async create(ctx?: FactoryContext) {
const config = await ctx?.inject(ConfigService)
const connection = {
host: config?.host ?? 'localhost',
port: config?.port ?? 5432,
connected: true,
}
// Register cleanup callback
ctx?.addDestroyListener(() => {
connection.connected = false
})
return connection
}
}
// Usage
const connection = await container.get(DatabaseConnectionFactory)
console.log(connection) // { host: 'localhost', port: 5432, connected: true }
Implement lifecycle hooks for initialization and cleanup:
import { Injectable, OnServiceDestroy, OnServiceInit } from '@navios/di'
@Injectable()
class DatabaseService implements OnServiceInit, OnServiceDestroy {
private connection: any = null
async onServiceInit() {
console.log('Initializing database connection...')
this.connection = await this.connect()
}
async onServiceDestroy() {
console.log('Closing database connection...')
if (this.connection) {
await this.connection.close()
}
}
private async connect() {
// Database connection logic
return { connected: true, close: async () => {} }
}
}
Use injection tokens for flexible dependency resolution:
import { Container, Injectable, InjectionToken } from '@navios/di'
import { z } from 'zod'
const configSchema = z.object({
apiUrl: z.string(),
timeout: z.number(),
})
const CONFIG_TOKEN = InjectionToken.create<z.infer<typeof configSchema>, typeof configSchema>(
'APP_CONFIG',
configSchema,
)
@Injectable({ token: CONFIG_TOKEN })
class ConfigService {
constructor(private config: z.infer<typeof configSchema>) {}
getApiUrl() {
return this.config.apiUrl
}
}
// Usage
const container = new Container()
const config = await container.get(CONFIG_TOKEN, {
apiUrl: 'https://api.example.com',
timeout: 5000,
})
Pre-bind values to injection tokens:
const BoundConfig = InjectionToken.bound(CONFIG_TOKEN, {
apiUrl: 'https://api.example.com',
timeout: 5000,
})
// No need to provide arguments
const container = new Container()
const config = await container.get(BoundConfig)
Use factories to resolve token values:
const FactoryConfig = InjectionToken.factory(CONFIG_TOKEN, async () => {
// Load config from environment or external source
return {
apiUrl: process.env.API_URL || 'https://api.example.com',
timeout: parseInt(process.env.TIMEOUT || '5000'),
}
})
const config = await container.get(FactoryConfig)
Instead of creating an injection token with a schema, you can directly provide a schema to the @Injectable decorator:
import { Injectable } from '@navios/di'
import { z } from 'zod'
const databaseConfigSchema = z.object({
host: z.string(),
port: z.number(),
username: z.string(),
password: z.string(),
})
@Injectable({ schema: databaseConfigSchema })
class DatabaseConfig {
constructor(public readonly config: z.output<typeof databaseConfigSchema>) {}
getConnectionString() {
return `${this.config.host}:${this.config.port}`
}
}
// Usage with arguments
const container = new Container()
const config = await container.get(DatabaseConfig, {
host: 'localhost',
port: 5432,
username: 'admin',
password: 'secret',
})
console.log(config.getConnectionString()) // "localhost:5432"
const dbConfigSchema = z.object({
connectionString: z.string(),
})
@Injectable({ schema: dbConfigSchema })
class DatabaseConfig {
constructor(public readonly config: z.output<typeof dbConfigSchema>) {}
}
@Injectable()
class DatabaseService {
// Inject with bound arguments
private dbConfig = inject(DatabaseConfig, {
connectionString: 'postgres://localhost:5432/myapp',
})
connect() {
return `Connecting to ${this.dbConfig.config.connectionString}`
}
}
The library automatically detects circular dependencies and provides helpful error messages:
@Injectable()
class ServiceA {
// Use asyncInject to break circular dependency
private serviceB = asyncInject(ServiceB)
async doSomething() {
const b = await this.serviceB
return b.getValue()
}
}
@Injectable()
class ServiceB {
private serviceA = inject(ServiceA)
getValue() {
return 'value from B'
}
}
import { Container, Registry } from '@navios/di'
const customRegistry = new Registry()
const container = new Container(customRegistry)
// Get all registrations for a token (sorted by priority, highest first)
const allRegistrations = customRegistry.getAll(MyToken)
import { DIError, DIErrorCode } from '@navios/di'
try {
const service = await container.get(NonExistentService)
} catch (error) {
if (error instanceof DIError) {
switch (error.code) {
case DIErrorCode.FactoryNotFound:
console.error('Service not registered')
break
case DIErrorCode.InstanceDestroying:
console.error('Service is being destroyed')
break
case DIErrorCode.ScopeMismatchError:
console.error('Wrong container for scope')
break
case DIErrorCode.TokenValidationError:
console.error('Token validation failed')
break
// ... and more error codes
}
}
}
// Invalidate a specific service and its dependencies
await container.invalidate(myService)
// The service will be recreated on next access
const newService = await container.get(MyService)
get<T>(token: T, args?): Promise<T> - Get an instanceinvalidate(service: unknown): Promise<void> - Invalidate a serviceready(): Promise<void> - Wait for pending operationsdispose(): Promise<void> - Clean up all resourcesclear(): Promise<void> - Clear all instances and bindingsisRegistered(token: any): boolean - Check if service is registeredcalculateInstanceName(token, args?): string | null - Calculate the instance name for a token (returns null for unresolved factory tokens or validation errors)getRegistry(): Registry - Get the registrybeginRequest(requestId: string, metadata?, priority?): ScopedContainer - Begin request contextgetActiveRequestIds(): ReadonlySet<string> - Get active request IDshasActiveRequest(requestId: string): boolean - Check if request is activeremoveRequestId(requestId: string): void - Remove a request ID from trackinggetStorage(): UnifiedStorage - Get storage instancegetServiceInitializer(): ServiceInitializer - Get service initializergetServiceInvalidator(): ServiceInvalidator - Get service invalidatorgetTokenResolver(): TokenResolver - Get token resolvergetNameResolver(): NameResolver - Get name resolvergetScopeTracker(): ScopeTracker - Get scope trackergetEventBus(): LifecycleEventBus - Get event busgetInstanceResolver(): InstanceResolver - Get instance resolverget<T>(token: T, args?): Promise<T> - Get an instance (request-scoped or delegated)invalidate(service: unknown): Promise<void> - Invalidate a serviceendRequest(): Promise<void> - End request and cleanupdispose(): Promise<void> - Alias for endRequest()ready(): Promise<void> - Wait for pending operationsisRegistered(token: any): boolean - Check if service is registeredcalculateInstanceName(token, args?): string | null - Calculate the instance name for a token (returns null for unresolved factory tokens or validation errors)getMetadata(key: string): any - Get request metadatasetMetadata(key: string, value: any): void - Set request metadatagetRequestId(): string - Get the request IDgetParent(): Container - Get the parent containergetStorage(): UnifiedStorage - Get the underlying storage instance@Injectable(options?: InjectableOptions) - Mark class as injectablescope?: InjectableScope - Service scope (Singleton | Transient | Request)token?: InjectionToken - Custom injection tokenschema?: ZodSchema - Zod schema for constructor argumentsregistry?: Registry - Custom registrypriority?: number - Priority level (higher wins when multiple registrations exist, default: 0)token and schema options together@Factory(options?: FactoryOptions) - Mark class as factoryscope?: InjectableScope - Factory scopetoken?: InjectionToken - Custom injection tokenregistry?: Registry - Custom registryinject<T>(token: T, args?): T - Synchronous injectionasyncInject<T>(token: T, args?): Promise<T> - Asynchronous injectionoptional<T>(token: T, args?): T | null - Optional injectionwrapSyncInit<T>(fn: () => T): T - Wrap synchronous initializationprovideFactoryContext<T>(ctx: FactoryContext, fn: () => T): T - Provide factory contextInjectionToken.create<T>(name: string | symbol): InjectionToken<T>InjectionToken.create<T, S>(name: string | symbol, schema: S): InjectionToken<T, S>InjectionToken.bound<T, S>(token: InjectionToken<T, S>, value: z.input<S>): BoundInjectionToken<T, S>InjectionToken.factory<T, S>(token: InjectionToken<T, S>, factory: () => Promise<z.input<S>>): FactoryInjectionToken<T, S>OnServiceInit - Implement onServiceInit(): Promise<void> | voidOnServiceDestroy - Implement onServiceDestroy(): Promise<void> | voidset(token, scope, target, type, priority?): void - Register a service factoryget(token): FactoryRecord - Get the highest priority factory record for a tokengetAll(token): FactoryRecord[] - Get all factory records for a token (sorted by priority, highest first)has(token): boolean - Check if a token is registereddelete(token): void - Remove all registrations for a tokenupdateScope(token, scope): boolean - Update the scope of an already registered factoryTestContainer extends Container with enhanced testing utilities, including fluent binding API, assertion helpers, method call tracking, and dependency graph inspection.
import { TestContainer } from '@navios/di/testing'
describe('UserService', () => {
let container: TestContainer
beforeEach(() => {
container = new TestContainer()
})
afterEach(async () => {
await container.clear()
})
it('should create user', async () => {
// Fluent binding API
container.bind(DatabaseService).toValue({
save: vi.fn().mockResolvedValue({ id: '1' }),
})
// Or bind to class
container.bind(UserService).toClass(MockUserService)
// Or bind to factory
container.bind(ConfigToken).toFactory(() => ({ apiKey: 'test' }))
const userService = await container.get(UserService)
const user = await userService.create({ name: 'John' })
expect(user.id).toBe('1')
// Assertion helpers
container.expectResolved(UserService)
container.expectSingleton(UserService)
container.expectInitialized(UserService)
// Method call tracking
container.recordMethodCall(UserService, 'create', [{ name: 'John' }], user)
container.expectCalled(UserService, 'create')
container.expectCalledWith(UserService, 'create', [{ name: 'John' }])
container.expectCallCount(UserService, 'create', 1)
// Dependency graph inspection
const graph = container.getDependencyGraph()
console.log(graph)
})
})
UnitTestContainer provides strict isolated unit testing with automatic method call tracking via Proxy. Only services explicitly provided can be resolved.
import { UnitTestContainer } from '@navios/di/testing'
describe('UserService Unit Tests', () => {
let container: UnitTestContainer
beforeEach(() => {
container = new UnitTestContainer({
providers: [
{ token: UserService, useClass: MockUserService },
{ token: ConfigToken, useValue: { apiUrl: 'test' } },
{ token: ApiClient, useFactory: () => new MockApiClient() },
],
})
})
afterEach(async () => {
await container.dispose()
})
it('should track method calls automatically', async () => {
const service = await container.get(UserService)
await service.findUser('123')
// Auto-tracked assertions (no manual recording needed)
container.expectCalled(UserService, 'findUser')
container.expectCalledWith(UserService, 'findUser', ['123'])
container.expectNotCalled(UserService, 'deleteUser')
})
it('should throw on unregistered dependencies (strict mode)', async () => {
// Strict mode (default): unregistered dependencies throw
await expect(container.get(UnregisteredService)).rejects.toThrow(DIError)
})
it('should auto-mock unregistered dependencies', async () => {
// Enable auto-mocking mode
container.enableAutoMocking()
const mock = await container.get(UnregisteredService)
container.expectAutoMocked(UnregisteredService)
container.disableAutoMocking()
})
})
asyncInject for circular dependencies - Breaks circular dependency cycles safelyinject for simple dependencies - When you're certain the dependency is readyoptional for feature flags - Dependencies that may not be availableIf you cannot use Stage 3 (native ES) decorators—for example, when working with existing TypeScript projects that have experimentalDecorators enabled, certain bundler configurations, or Bun—you can use the legacy-compatible decorators:
import { Injectable, Factory } from '@navios/di/legacy-compat'
import { inject, asyncInject, Container } from '@navios/di'
@Injectable()
class UserService {
private readonly db = inject(DatabaseService)
}
const container = new Container()
const userService = await container.get(UserService)
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
MIT
FAQs
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.