Dependency Injection
Navios Commander has full integration with @navios/di, allowing you to inject services into commands and use all DI features. This guide covers how to use dependency injection in your CLI commands.
Basic Injection
Inject services into commands using the inject() function:
import { Command, CommandHandler } from '@navios/commander'
import { inject, Injectable } from '@navios/di'
@Injectable()
class UserService {
async getUser(id: string) {
return { id, name: 'John Doe', email: '[email protected]' }
}
}
@Command({ path: 'user:show' })
export class ShowUserCommand implements CommandHandler {
private userService = inject(UserService)
async execute(options: { userId: string }) {
const user = await this.userService.getUser(options.userId)
console.log(`User: ${user.name} (${user.email})`)
}
}
Service Scopes
Services can have different scopes, just like in @navios/di:
Singleton Services
Singleton services are shared across all command executions:
import { Injectable, InjectableScope } from '@navios/di'
@Injectable({ scope: InjectableScope.Singleton })
class ConfigService {
private config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
}
getApiUrl() {
return this.config.apiUrl
}
}
@Command({ path: 'api:call' })
export class ApiCallCommand implements CommandHandler {
private config = inject(ConfigService)
async execute() {
const url = this.config.getApiUrl()
console.log(`Calling ${url}`)
}
}
Transient Services
Transient services create a new instance for each injection:
import { Injectable, InjectableScope } from '@navios/di'
@Injectable({ scope: InjectableScope.Transient })
class LogEntry {
private id = Math.random().toString(36)
getId() {
return this.id
}
}
@Command({ path: 'log' })
export class LogCommand implements CommandHandler {
private logEntry = inject(LogEntry)
async execute() {
console.log(`Log entry ID: ${this.logEntry.getId()}`)
}
}
Request-Scoped Services
Request-scoped services are created per command execution:
import { Injectable, InjectableScope } from '@navios/di'
@Injectable({ scope: InjectableScope.Request })
class CommandContext {
private executionId = Math.random().toString(36)
getExecutionId() {
return this.executionId
}
}
@Command({ path: 'process' })
export class ProcessCommand implements CommandHandler {
private context = inject(CommandContext)
async execute() {
console.log(`Execution ID: ${this.context.getExecutionId()}`)
}
}
Injection Methods
inject() - Synchronous Injection
Use inject() for immediate access to dependencies:
@Command({ path: 'user:show' })
export class ShowUserCommand implements CommandHandler {
private userService = inject(UserService)
async execute(options: { userId: string }) {
// Direct access - no await needed
const user = await this.userService.getUser(options.userId)
console.log(user)
}
}
asyncInject() - Asynchronous Injection
Use asyncInject() for explicit async control or to break circular dependencies:
@Command({ path: 'user:show' })
export class ShowUserCommand implements CommandHandler {
private userService = asyncInject(UserService)
async execute(options: { userId: string }) {
const service = await this.userService
const user = await service.getUser(options.userId)
console.log(user)
}
}
optional() - Optional Injection
Use optional() to inject a dependency only if it's available:
import { optional } from '@navios/di'
@Command({ path: 'notify' })
export class NotifyCommand implements CommandHandler {
private emailService = optional(EmailService)
async execute(options: { message: string }) {
// Only sends email if EmailService is available
this.emailService?.send(options.message)
}
}
Service Dependencies
Services can depend on other services:
@Injectable()
class DatabaseService {
async query(sql: string) {
// Database query logic
return { rows: [] }
}
}
@Injectable()
class UserRepository {
private db = inject(DatabaseService)
async findById(id: string) {
return this.db.query(`SELECT * FROM users WHERE id = '${id}'`)
}
}
@Command({ path: 'user:show' })
export class ShowUserCommand implements CommandHandler {
private userRepo = inject(UserRepository)
async execute(options: { userId: string }) {
const user = await this.userRepo.findById(options.userId)
console.log(user)
}
}
Injection Tokens
Use injection tokens for flexible dependency resolution:
import { Injectable, InjectionToken } from '@navios/di'
interface PaymentProcessor {
processPayment(amount: number): Promise<string>
}
const PAYMENT_PROCESSOR_TOKEN = InjectionToken.create<PaymentProcessor>(
'PaymentProcessor',
)
@Injectable({ token: PAYMENT_PROCESSOR_TOKEN })
class StripePaymentProcessor implements PaymentProcessor {
async processPayment(amount: number) {
return `Processed $${amount} via Stripe`
}
}
@Command({ path: 'pay' })
export class PayCommand implements CommandHandler {
private processor = inject(PAYMENT_PROCESSOR_TOKEN)
async execute(options: { amount: number }) {
const result = await this.processor.processPayment(options.amount)
console.log(result)
}
}
Configuration Services
Use injection tokens with Zod schemas for type-safe configuration:
import { Injectable, InjectionToken } from '@navios/di'
import { z } from 'zod'
const configSchema = z.object({
apiUrl: z.string().url(),
timeout: z.number().min(1000),
})
const CONFIG_TOKEN = InjectionToken.create<ConfigService, typeof configSchema>(
'APP_CONFIG',
configSchema,
)
@Injectable({ token: CONFIG_TOKEN })
class ConfigService {
constructor(private config: z.infer<typeof configSchema>) {}
getApiUrl() {
return this.config.apiUrl
}
getTimeout() {
return this.config.timeout
}
}
@Command({ path: 'api:call' })
export class ApiCallCommand implements CommandHandler {
private config = inject(CONFIG_TOKEN, {
apiUrl: 'https://api.example.com',
timeout: 5000,
})
async execute() {
console.log(`API URL: ${this.config.getApiUrl()}`)
console.log(`Timeout: ${this.config.getTimeout()}ms`)
}
}
Learn more about configuration services in DI recipes.
Lifecycle Hooks
Services can implement lifecycle hooks:
import { Injectable, OnServiceInit, OnServiceDestroy } from '@navios/di'
@Injectable()
class DatabaseService implements OnServiceInit, OnServiceDestroy {
private connection: any = null
async onServiceInit() {
console.log('Connecting to database...')
this.connection = await this.connect()
console.log('Database connected')
}
async onServiceDestroy() {
console.log('Disconnecting from database...')
if (this.connection) {
await this.connection.close()
}
}
private async connect() {
// Connection logic
return { connected: true }
}
}
@Command({ path: 'db:query' })
export class QueryCommand implements CommandHandler {
private db = inject(DatabaseService)
async execute(options: { sql: string }) {
// Database is already connected
const result = await this.db.query(options.sql)
console.log(result)
}
}
Request Context
Each command execution runs in its own request context, allowing for request-scoped services:
@Injectable({ scope: InjectableScope.Request })
class RequestLogger {
private logs: string[] = []
log(message: string) {
this.logs.push(message)
}
getLogs() {
return this.logs
}
}
@Command({ path: 'process' })
export class ProcessCommand implements CommandHandler {
private logger = inject(RequestLogger)
async execute() {
this.logger.log('Starting process')
// Process logic
this.logger.log('Process complete')
const logs = this.logger.getLogs()
console.log('Logs:', logs)
}
}
Accessing the Container
You can access the DI container from the application:
import { CommanderFactory } from '@navios/commander'
import { AppModule } from './app.module'
async function bootstrap() {
const app = await CommanderFactory.create(AppModule)
await app.init()
const container = app.getContainer()
const service = await container.get(MyService)
await app.run(process.argv)
await app.close()
}
bootstrap()
Best Practices
1. Use Singleton for Stateless Services
// ✅ Good: Stateless service as singleton
@Injectable({ scope: InjectableScope.Singleton })
class EmailService {
async sendEmail(to: string, subject: string) {
// No state, safe to share
}
}
2. Use Request Scope for Command-Specific State
// ✅ Good: Request-scoped for command state
@Injectable({ scope: InjectableScope.Request })
class CommandContext {
private data: any = {}
setData(key: string, value: any) {
this.data[key] = value
}
}
3. Avoid Accessing Services in Constructors
// ❌ Avoid: Accessing services in constructor
@Command({ path: 'example' })
export class ExampleCommand implements CommandHandler {
private service = inject(MyService)
constructor() {
// ❌ Service may not be ready
this.service.doSomething()
}
}
// ✅ Good: Use in execute method
@Command({ path: 'example' })
export class ExampleCommand implements CommandHandler {
private service = inject(MyService)
async execute() {
// ✅ Service is ready
await this.service.doSomething()
}
}
4. Use Injection Tokens for Interfaces
// ✅ Good: Use tokens for interfaces
interface PaymentProcessor {
processPayment(amount: number): Promise<string>
}
const PAYMENT_PROCESSOR_TOKEN = InjectionToken.create<PaymentProcessor>(
'PaymentProcessor',
)
@Injectable({ token: PAYMENT_PROCESSOR_TOKEN })
class StripeProcessor implements PaymentProcessor {
// ...
}
Next Steps
- Learn more about DI in Navios
- Explore service scopes
- Check out injection tokens
- See execution context for command-specific context