DEV Community

Jay McDoniel for NestJS

Posted on • Edited on

Introducing nest-commander

Jay is a member of the NestJS core team, primarily helping out the community on Discord and Github and contributing to various parts of the framework.

What's up Nestlings! So you've been developing web servers with NestJS for a while now, and you're in love with the DI context that it brings. You're enthralled by the class first approach, and you have "nest-itis" where you just want everything to work with Nest because, well, it just feels good. But now, after building some web servers and application, you realize you need to be able to run some one-off methods, or maybe you want to provide a full CLI package for one of your projects. You could write these commands using shell scripts (and I wish you the best if so), or you could use a CLI package like yargs or commander. Those are all fine options, but none of them are using Nest, which is sad for those of you with "nest-itis". So what do you do? You go to Google, type in "nest commander package" and search through the results, finding nest-commander. And that's where you see the light.

What sets nest-commander apart from other Nest CLI packages

nestjs-command

  • uses yargs as the underlying CLI engine
  • uses parameter decorators for command
  • has a CommandModuleTest built into the package

nestjs-console

  • uses commander for the underlying CLI engine
  • has a ConsoleService to create commands or can use decorator
  • no immediate testing integration

nest-commander

  • uses commander for the underlying CLI engine
  • uses a decorator on a class for a command and a decorator on class methods for command options
  • has a separate testing package to not bloat the final CLI package size
  • has an InquirerService to integrate inquirer into your CLI application
  • has a CommandFactory to run similar to NestFactory for a familiar DX

How to use it

Okay, so we've talked about what's different, but let's see how we can actually write a command using nest-commander.

Let's say we want to create a CLI that takes in a name, and an age and outputs a greeting. Our CLI will have the inputs of -n personName and -a age. In commander itself, this would look something like

const program = new Command();
program
  .option('-n <personName>')
  .option('-a <age>');
program.parse(process.argv);
const options = program.options();
options.age = Number.parseInt(options.age, 10);
if (options.age < 13) {
  console.log(`Hello ${options.personName}, you're still rather young!`);
} else if (12 < options.age && options.age < 50) {
  console.log(`Hello ${options.personName}, you're in the prime of your life!`);
} else {
  console.log(`Hello ${options.personName}, getting up there in age, huh? Well, you're only as young as you feel!`);
}
Enter fullscreen mode Exit fullscreen mode

This works out well, and it pretty easy to run, but as your program grows it may be difficult to keep all of the logic clean and separated. Plus, in some cases you may need to re-instantiate services that Nest already manages for you. So enter, the @Command() decorator and the CommandRunner interface.

All nest-commander commands implement the CommandRunner interface, which says that every @Command() will have an async run(inputs: string[], options?: Record<string, any>): Promise<void> method. inputs are values that are passed directly to the command, as defined by the arguments property of the @Command() decorator. options are the options passed for the command that correlate back to each @Option() decorator. The above command could be written with nest-commander like so

@Command({ name: 'sayHello', options: { isDefault: true }})
export class SayHelloCommand implements CommandRunner {
  async run(inputs: string[], options: { name: string, age: number }): Promise<void> {
    if (options.age < 13) {
      console.log(`Hello ${options.personName}, you're still rather young!`);
    } else if (12 < options.age && options.age < 50) {
      console.log(`Hello ${options.personName}, you're in the prime of your life!`);
    } else {
      console.log(`Hello ${options.personName}, getting up there in age, huh? Well, you're only as young as you feel!`);
    }
  }

  @Option({ flags: '-n <personName>' })
  parseName(val: string) {
    return val;
  }

  @Option({ flags: '-a <age>' })
  parseAge(val: string) {
    return Number.parseInt(val, 10);
  }
}
Enter fullscreen mode Exit fullscreen mode

Now all we need to do is add the SayHelloCommand to the Nest application and make use of CommandFactory in our main.ts.

// src/say-hello.module.ts
@Module({
  providers: [SayHelloCommand],
})
export class SayHelloModule {}
Enter fullscreen mode Exit fullscreen mode
// src/main.ts
import { CommandFactory } from 'nest-commander';
import { SayHelloModule } from './say-hello.module';

async function bootstrap() {
  await CommandFactory.run(SayHelloModule);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

And there you have it, the command is fully operational. If you end up forgetting to pass in an option, commander will inform you the call is invalid. To run the command from here, compile your code as you normally would (either using nest build or tsc) and then run node dist/main.

Now, this is all fine and dandy, but the real magic, as mentioned before, is that all of Nest's DI context still works! So long as you are using singleton or transient providers, there's no limitation to what the CommandFactory can manage.

InquirerService

So now what? You've got this fancy CLI application and it runs awesome, but what about when you want to get user input during runtime, not just when starting the application. Well, that's where the InquirerService comes in. The first thing that needs to happen is a class with @QuestionSet() needs to be created. This will be the class that holds the questions for the named set. The name is important as it will be used in the InquirerService later. Say that we want to get the name and age at runtime or at start time, first we need to change the options to optional by changing from chevrons to brackets (i.e. <personName> to [personName]). Next, we need to create our question set

@QuestionSet({ name: 'personInfo' })
export class PersonInfoQuestions {
  @Question({
    type: 'input',
    name: 'personName',
    message: 'What is your name?'
  })
  parseName(val: string) {
    return val;
  }

  @Question({
    type: 'input',
    name: 'age',
    message: 'How old are you?'
  })
  parseAge(val: string) {
    return Number.parseInt(val, 10);
  }
}
Enter fullscreen mode Exit fullscreen mode

Now in the SayHelloCommand we need to add in the InquirerService and ask for the information.

@Command({ name: 'sayHello', options: { isDefault: true })
export class SayHelloCommand implements CommandRunner {
  constructor(private readonly inquirerService: InquirerService) {}

  async run(inputs: string[], options: { personName?: string, age?: number }): Promise<void> {
    options = await this.inquirerService.ask('personInfo', options);
    if (options.age < 13) {
      console.log(`Hello ${options.personName}, you're still rather young!`);
    } else if (12 < options.age && options.age < 50) {
      console.log(`Hello ${options.personName}, you're in the prime of your life!`);
    } else {
      console.log(`Hello ${options.personName}, getting up there in age, huh? Well, you're only as young as you feel!`);
    }
  }
...
Enter fullscreen mode Exit fullscreen mode

The rest of the class follows as above. Now we can pass in the options commander already found, and inquirer will skip over asking for them again, allowing for the a great UX by not having to duplicate their information (now if only resume services were so nice). Now in SayHelloModule we add in the PersonInfoQuestions to the providers and everything else just works ™️

@Module({
  providers: [
    SayHelloCommand,
    PersonInfoQuestions,
  ]
})
export class SayHelloModule {}
Enter fullscreen mode Exit fullscreen mode

And just like that, we've now created a command line application using nest-commander, allowing for users to pass the info in via flags or using prompts and asking for it at runtime.

For more information on the project you can check the repo here. There's also a testing package to help with testing both the commander input and the inquirer input. Feel free to raise any issues or use the #nest-commander channel on the official NestJS Discord

Top comments (18)

Collapse
 
simplenotezy profile image
Mattias Fjellvang • Edited

Also, by copying the example from this article, I would get:

Property 'personName' does not exist on type '{ name: string; age: number; }'.ts(2339)

I had to tweak the commend to look like this (also adding --name to option) before it would work:

paste.laravel.io/b566d7db-4791-48c...

Collapse
 
jmcdo29 profile image
Jay McDoniel

Thanks for reporting that. The post has been updated to have the proper values

Collapse
 
gktim profile image
gkTim

Nice, Great work!

Collapse
 
tony133 profile image
Antonio Tripodi

Fantastic and interesting post! Great!

Collapse
 
smolinari profile image
Scott Molinari

This is cool stuff. I'm going to be using it extensively in the next couple of weeks.

Scott

Collapse
 
avantar profile image
Krzysztof Szala

I was waiting for something like this. Awesome. Thanks!

Ps. You have a few typos in your code examples.

Collapse
 
jmcdo29 profile image
Jay McDoniel

Thanks for saying something! This is what I get for writing everything in the online editor and not in my IDE

Collapse
 
almangor7_f0e94590fd3dfa7 profile image
almangor7

this is copy and pasta, how to use this nest-commander in real project. You just copied this not working example and even dont think about that main.ts can be already used for API backend purpose and your example is very silly.

Collapse
 
sueric profile image
Eric

Very nice article! Will give it a try

Collapse
 
markpieszak profile image
Mark Pieszak

Love it Jay, great write up! 👏👏
(Btw tiny typo about in one code block you have @Opiton )

Collapse
 
jmcdo29 profile image
Jay McDoniel

Thanks Mark! Got it fixed

Collapse
 
piotr_jura profile image
Piotr Jura

I was looking at making my own solution, now I don't have to. Great, thanks!

Collapse
 
simplenotezy profile image
Mattias Fjellvang

Would be nice to show how to actually run the command - it took a bit of tinkering to figure out

Collapse
 
jmcdo29 profile image
Jay McDoniel

I added a quick note of how you can run the command after the setup of the src/main.ts file. There's more information as well on the docs site