I recently started a role on a company's developer experience team. One of the responsibilities that comes along with this is building larger CLI applications.
As I wrote previously, I use the commander
library when writing DevOps scripts for my NodeJS projects. This is great for smaller utilities that only a couple of people will use, but what about if it needs to interact with multiple services and have lots of nested commands?
As it turns out NestJS supports this using the same commander library that I used previously. In this article, I'll show you how to create a basic CLI using this setup.
Creating the Project
Creating the CLI project is the same as creating an API with NestJS. Simply follow the basic workflow on nestjs website.
npm i -g @nestjs/cli
nest new my-cli-project
cd my-cli-project
Setup nest-commander
The nest-commander
library provides all the code that you'll need to create your commander CLI in the background. Like every other node dependency, the first step is to install the package.
npm i nest-commander
Creating a Command
Just like with normal nestjs development, we need to create a module that our command will live in.
Nest-Commander provides some tools that plug into the nest cli. I've tried using them but wasn't able to get them to work. You may have better luck, but I'll continue with the manual process.
nest g module CowSay
Normally you would create a controller or resolver in your module using the nest cli. Since we're creating a cli, we want to create a Command
instead. Create a cow-say.command.ts
file in the src/cow-say
folder and open it.
Each command that you create will extend the CommandRunner
class and use the @Command()
decorator. In the cow-say.command.ts
file that you just created, add the following.
import { Command, CommandRunner } from 'nest-commander';
@Command({
name: 'cowsay',
options: {
isDefault: true,
},
})
export class CowSayCommand extends CommandRunner {
async run(): Promise<void> {
}
}
To get this command to display something, import the cowsay
library.
import * as cowsay from 'cowsay';
You'll need to install it too...
npm i cowsay
...and update the run
method in cow-say.command.ts
async run(): Promise<void> {
console.log(cowsay.say({ text: 'Hello World!' }));
}
Wiring Everything Together
Now that the command is created, you need to register it as part of the module. Open the cow-say.module.ts
file and add CowSayCommand
as a provider.
import { Module } from '@nestjs/common';
import { CowSayCommand } from './cow-say.command';
@Module({
providers: [CowSayCommand],
})
export class CowSayModule {}
Tell NestJS that it's not a server
Now comes the tricky part. The nest project that you created is setup to create a REST API by default, but you don't need that. So delete the app service and controller.
rm app.service.ts
rm app.controller.ts
Next, update the AppModule
to import the CowSay
module.
import { Module } from '@nestjs/common';
import { CowSayModule } from './cow-say/cow-say.module';
@Module({
imports: [CowSayModule],
})
export class AppModule {}
Finally, you need to update the main.ts
file. You'll change the bootstrap
function to use CommandFactory
instead of NestFactory
.
import { CommandFactory } from 'nest-commander';
import { AppModule } from './app.module';
async function bootstrap() {
await CommandFactory.run(AppModule);
}
bootstrap();
Running It
I like to have the option of running my CLI tools without having to run a TypeScript build step. This helps speed up development.
To run your CLI without building it, you'll be using the ts-node
package. To get started install it as a development dependency.
npm i -D ts-node
Now add a new script to package.json
"start:cli": "ts-node src/main.ts"
...and you can test your CLI by running the script
❯ npm run start:cli
> my-cli-project@0.0.1 start:cli
> ts-node src/main.ts
______________
< Hello World! >
--------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
Summary
This article showed how to get started building your very first NestJS CLI application. Taking this approach can help you build a larger application that is maintainable. In future articles I'll be introducing you to more advanced features of this setup.