Skip to main content

Best Practices

This guide covers best practices for building CLI applications with Navios Commander.

Command Design

Use Descriptive Command Paths

Use clear, descriptive command paths that indicate what the command does:

// ✅ Good: Clear and descriptive
@Command({ path: 'user:create' })
@Command({ path: 'database:migrate' })
@Command({ path: 'deploy:production' })

// ❌ Avoid: Vague or unclear
@Command({ path: 'create' })
@Command({ path: 'migrate' })
@Command({ path: 'deploy' })

Use Namespaces for Organization

Group related commands with namespaces:

// ✅ Good: Organized with namespaces
@Command({ path: 'user:create' })
@Command({ path: 'user:delete' })
@Command({ path: 'user:list' })

// ❌ Avoid: Flat structure
@Command({ path: 'create-user' })
@Command({ path: 'delete-user' })
@Command({ path: 'list-users' })

Keep Commands Focused

Each command should do one thing well:

// ✅ Good: Focused command
@Command({ path: 'user:create' })
export class CreateUserCommand implements CommandHandler {
async execute(options: { name: string; email: string }) {
// Only creates users
}
}

// ❌ Avoid: Doing too much
@Command({ path: 'user:manage' })
export class ManageUserCommand implements CommandHandler {
async execute(options: { action: string; ... }) {
// Creates, updates, deletes - too many responsibilities
}
}

Validation

Always Validate Options

Always provide schemas for command options:

// ✅ Good: Validated options
@Command({
path: 'create-user',
optionsSchema: z.object({
name: z.string().min(1),
email: z.string().email(),
}),
})

// ❌ Avoid: No validation
@Command({ path: 'create-user' })
export class CreateUserCommand implements CommandHandler {
async execute(options: any) {
// No type safety or validation
}
}

Use Descriptive Error Messages

Provide clear error messages in your schemas:

// ✅ Good: Clear error messages
const schema = z.object({
age: z.number().min(18, 'Must be at least 18 years old'),
email: z.string().email('Invalid email address'),
})

// ❌ Avoid: Generic error messages
const schema = z.object({
age: z.number().min(18),
email: z.string().email(),
})

Provide Default Values

Use default values for optional options:

// ✅ Good: Default values
const schema = z.object({
verbose: z.boolean().default(false),
timeout: z.number().default(5000),
format: z.enum(['json', 'table']).default('json'),
})

// ❌ Avoid: Required optional fields
const schema = z.object({
verbose: z.boolean().optional(), // User must always provide
})

Module Organization

Organize commands by feature or domain:

// ✅ Good: Commands grouped by feature
@CliModule({
commands: [
CreateUserCommand,
DeleteUserCommand,
ListUsersCommand,
],
})
export class UserModule {}

// ❌ Avoid: Unrelated commands together
@CliModule({
commands: [
CreateUserCommand,
MigrateCommand,
SendEmailCommand,
],
})
export class MixedModule {}

Use Feature Modules

Create modules for each feature area:

// ✅ Good: Feature-based modules
@CliModule({ commands: [...] })
export class UserModule {}

@CliModule({ commands: [...] })
export class DatabaseModule {}

@CliModule({ commands: [...] })
export class EmailModule {}

@CliModule({
imports: [UserModule, DatabaseModule, EmailModule],
})
export class AppModule {}

Keep Modules Focused

Each module should have a clear purpose:

// ✅ Good: Focused module
@CliModule({
commands: [
CreateUserCommand,
UpdateUserCommand,
DeleteUserCommand,
],
})
export class UserModule {}

// ❌ Avoid: Module doing too much
@CliModule({
commands: [
CreateUserCommand,
MigrateCommand,
SendEmailCommand,
ProcessPaymentCommand,
],
})
export class EverythingModule {}

Dependency Injection

Use Singleton for Stateless Services

Use singleton scope 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
}
}

Use Request Scope for Command-Specific State

Use request scope for services that need 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
}
}

Avoid Accessing Services in Constructors

Don't access injected 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()
}
}

Error Handling

Provide Meaningful Error Messages

Give users helpful error messages:

// ✅ Good: Helpful error messages
@Command({ path: 'delete-user' })
export class DeleteUserCommand implements CommandHandler {
async execute(options: { userId: string }) {
const user = await this.userService.getUser(options.userId)
if (!user) {
throw new Error(`User ${options.userId} not found`)
}
await this.userService.deleteUser(options.userId)
}
}

// ❌ Avoid: Generic error messages
@Command({ path: 'delete-user' })
export class DeleteUserCommand implements CommandHandler {
async execute(options: { userId: string }) {
const user = await this.userService.getUser(options.userId)
if (!user) {
throw new Error('Error') // Not helpful
}
}
}

Handle Validation Errors Gracefully

Catch and handle validation errors:

// ✅ Good: Handle validation errors
@Command({ path: 'create-user' })
export class CreateUserCommand implements CommandHandler {
async execute(options: { name: string; email: string }) {
try {
await this.userService.createUser(options)
} catch (error) {
if (error instanceof ZodError) {
console.error('Validation error:', error.errors)
process.exit(1)
}
throw error
}
}
}

Output and Formatting

Support Multiple Output Formats

Allow users to choose output format:

// ✅ Good: Multiple output formats
@Command({
path: 'list-users',
optionsSchema: z.object({
format: z.enum(['json', 'table', 'csv']).default('table'),
}),
})
export class ListUsersCommand implements CommandHandler {
async execute(options) {
const users = await this.userService.getAllUsers()

if (options.format === 'json') {
console.log(JSON.stringify(users, null, 2))
} else if (options.format === 'csv') {
this.printCsv(users)
} else {
this.printTable(users)
}
}
}

Use Consistent Formatting

Keep output formatting consistent:

// ✅ Good: Consistent formatting
@Command({ path: 'user:show' })
export class ShowUserCommand implements CommandHandler {
async execute(options: { userId: string }) {
const user = await this.userService.getUser(options.userId)
console.log(`User ID: ${user.id}`)
console.log(`Name: ${user.name}`)
console.log(`Email: ${user.email}`)
}
}

Testing

Test Commands in Isolation

Test commands independently:

// ✅ Good: Isolated test
it('should create user', async () => {
const app = await CommanderFactory.create(TestModule)
await app.init()

await app.executeCommand('user:create', {
name: 'John',
email: '[email protected]',
})

await app.close()
})

Mock External Dependencies

Mock services that interact with external systems:

// ✅ Good: Mocked external service
const mockEmailService = {
send: jest.fn().mockResolvedValue(true),
}

Performance

Lazy Load Heavy Dependencies

Load heavy dependencies only when needed:

// ✅ Good: Lazy loading
@Command({ path: 'process' })
export class ProcessCommand implements CommandHandler {
private heavyService = asyncInject(HeavyService)

async execute() {
const service = await this.heavyService
// Use service only when needed
}
}

Cache Expensive Operations

Cache results of expensive operations:

// ✅ Good: Caching
@Injectable()
class ConfigService {
private cache: any = null

async getConfig() {
if (!this.cache) {
this.cache = await this.loadConfig()
}
return this.cache
}
}

Security

Validate All Input

Always validate user input:

// ✅ Good: Validated input
@Command({
path: 'delete-file',
optionsSchema: z.object({
file: z.string().refine(
(path) => !path.includes('..'),
{ message: 'Invalid file path' }
),
}),
})

Sanitize Output

Sanitize output to prevent injection attacks:

// ✅ Good: Sanitized output
@Command({ path: 'execute' })
export class ExecuteCommand implements CommandHandler {
async execute(options: { command: string }) {
// Sanitize command before execution
const sanitized = this.sanitize(options.command)
// Execute sanitized command
}
}

Documentation

Document Command Options

Provide clear documentation for command options:

// ✅ Good: Documented options
const schema = z.object({
// User's full name
name: z.string().min(1),
// User's email address (must be valid email)
email: z.string().email(),
// User's age (must be 18 or older)
age: z.number().min(18),
})

Add Help Text

Consider adding help text to commands:

// ✅ Good: Help text
@Command({ path: 'user:create' })
export class CreateUserCommand implements CommandHandler {
async execute(options: { name: string; email: string }) {
// Command implementation
}
}

// Usage: node cli.js user:create --name "John" --email "[email protected]"

Common Patterns

Configuration Commands

Use commands for configuration:

@Command({
path: 'config:set',
optionsSchema: z.object({
key: z.string(),
value: z.string(),
}),
})
export class SetConfigCommand implements CommandHandler {
async execute(options) {
// Set configuration
}
}

Status Commands

Provide status commands:

@Command({ path: 'status' })
export class StatusCommand implements CommandHandler {
async execute() {
// Show application status
}
}

Health Check Commands

Include health check commands:

@Command({ path: 'health' })
export class HealthCommand implements CommandHandler {
async execute() {
// Check system health
}
}

Next Steps