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
Group Related Commands
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
- Review the guides for detailed information
- Check out recipes for common patterns
- See the API reference for complete details