Using Node.js to Create Powerful, Beautiful, User-Friendly CLIs
Not every Node.js application is meant to live in the web; Node.js is a popular runtime allowing you to write multiple types of applications running on a variety of platforms—from the cloud to many IoT devices. Naturally, Node.js can also run in your local shell, where powerful tools can perform magic, executing useful tasks enhancing your developer capabilities.
A Command Line Interface (CLI), can perform anything from a simple operation—like printing ASCII art in the terminal like yosay—to entirely generating the code for a project based on your choices using multiple templates like Yeoman yo. These programs can be installed globally from npm, or executed directly using npx
if they are simple enough.
Let's explore the basics of building a simple CLI using Node.js. In this example, we’re creating a simple command which receives a name as an argument displaying an emoji and a greeting.
The first thing you should do as in every application is to create a folder for it and execute:
$ npm init
The previous command will ask for some information like the package name, version, license, and others creating the package.json
at the end, looking like this:
{
"name": "hello-emoji",
"version": "1.0.0",
"description": "A hello world CLI with a nice emoji",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "edsadr",
"license": "MIT"
}
As we want our CLI to be available as a command, we need to configure our package.json
to treat our program like that, to do it we add a bin
section like this:
"bin": {
"hello-emoji": "./index.js"
}
In this case, hello-emoji
is the command we are registering to execute our program, and ./index.js
is the file to be executed when the command is invoked.
To display emojis, let's add a package:
$ npm install node-emoji -S
Now, let's create the file to be executed, index.js
:
#!/usr/bin/env node
'use strict'
const emojis = require('node-emoji')
if (!process.argv[2]) {
console.error(`${emojis.get('no_entry')} Please add your name to say hello`)
process.exit(1)
}
console.log(`${emojis.random().emoji} Hello ${process.argv['2']}!`)
Note that we add #!/usr/bin/env node
at the top. This tells the system what interpreter to pass that file to for execution; in our case the interpreter is Node.js. After that, the code is fairly straightforward. It requires the node-emoji
module and validates process.argv[2]
, which is the first argument placed by the user. By default process.argv[0]
is the path for Node.js binary, and process.argv[1]
is the path for the script being executed.
After adding this code, our command is ready to be executed; you can get a 'Hello world!' in your console by running:
$ node index.js world
If you want to run it using the command specified at the bin section of our package.json
, you’ll need to install the package globally from npm. But just for development purposes to run it locally we can use:
$ npm link
After executing this command, you can try to execute:
$ hello-emoji world
Arguments parsing
After examining the code we just wrote, you’ll likely realize that the main issue when writing this kind of application is to control the user's input parsing the arguments included in the command execution. Fortunately, the npm ecosystem offers plenty of choices to solve this problem.
Here are some modules helping you to parse user-entered arguments. Some even provide some guidelines to structure your CLI's code:
- Yargs: https://www.npmjs.com/package/yargs
- Minimist: https://www.npmjs.com/package/minimist
- Commander: https://www.npmjs.com/package/commander
- Args: https://www.npmjs.com/package/args
These packages allow you to create a CLI supporting multiple operations and include parameters; you could efficiently structure something for our CLI to do things like:
$ hello-emoji --name=world --json
Printing a JSON object with our greeting
$ hello-emoji --name=world --emoji=coffee
Instead of a random emoji, this one prints the coffee emoji
Here is an example implementing minimist to do the parsing to execute commands like the ones above:
#!/usr/bin/env node
'use strict'
const emojis = require('node-emoji')
const minimist = require('minimist')
const opts = minimist(process.argv.slice(2))
const emoji = opts.emoji ? emojis.get(opts.emoji) : emojis.random().emoji
if (!opts.name) {
console.error(`${emojis.get('no_entry')} Please add your name to say hello using the '--name=' parameter`)
process.exit(1)
}
if (!emojis.has(opts.emoji)) {
console.error(`${opts.emoji} is not a valid emoji, please check https://www.webfx.com/tools/emoji-cheat-sheet/`)
process.exit(1)
}
const greeting = `${emoji} Hello ${opts.name}!`
if (opts.json) {
console.log(JSON.stringify({greeting}))
} else {
console.log(greeting)
}
Going interactive
So far, we have been working with information coming from the command execution. However, there is also another way to help make your CLI more interactive and request information at execution time. These modules can help to create a better experience for the user:
- Inquirer: https://www.npmjs.com/package/inquirer
- Prompts: https://www.npmjs.com/package/prompts
- Prompt: https://www.npmjs.com/package/prompt
- Enquirer: https://www.npmjs.com/package/enquirer
With a package like the ones above, you could ask the user directly to input the desired information in many different styles. The example below is using inquirer
to ask the users for the name if it was not included as an argument. It also validates the emoji and requests a new one if the input is not valid.
#!/usr/bin/env node
'use strict'
const emojis = require('node-emoji')
const inquirer = require('inquirer')
const minimist = require('minimist')
const opts = minimist(process.argv.slice(2))
let emoji = opts.emoji ? emojis.get(opts.emoji) : emojis.random().emoji
async function main () {
if (!opts.name) {
const askName = await inquirer.prompt([{
type: 'input',
name: 'name',
message: `Please tell us your name: `,
default: () => 'world',
validate: (answer) => answer.length >= 2
}])
opts.name = askName.name
}
if (opts.emoji && !emojis.hasEmoji(opts.emoji)) {
console.error(`${opts.emoji} is not a valid emoji, please check https://www.webfx.com/tools/emoji-cheat-sheet/`)
const askEmoji = await inquirer.prompt([{
type: 'input',
name: 'emoji',
message: `Please input a valid emoji: `,
default: () => 'earth_americas',
validate: (emoji) => emojis.hasEmoji(emoji)
}])
emoji = emojis.get(askEmoji.emoji)
}
const greeting = `${emoji} Hello ${opts.name}!`
if (opts.json) {
console.log(JSON.stringify({
greeting
}))
} else {
console.log(greeting)
}
}
main()
Adding some Eye Candy
Even if the interface for this kind of application is reduced to what you can have in a shell, it does not mean that the UI should look bad. There are plenty of tools that can help make your apps look good; here are some different libraries that will add a nice touch to the look of your CLI output:
- Chalk or Colors will allow you to set the color of your text.
- To include images translated to ASCII art, try asciify-image or ascii-art
- If you have to output much information a well-organized output could be in tables, try Table or Cli-table
- If your CLI requires processes taking some time, like consuming external APIs, querying databases or even writing files, you can add a cute spinner with Ora or Cli-spinner.
Conclusion
Creating user-friendly, useful and beautiful CLIs is part science and part art. After exploring the basics of creating a CLI tool, you can go and explore a universe of possibilities with the packages available through the npm registry. Hopefully, you’ll soon be creating functional and user-friendly tooling that’s missing from your current inventory thanks to the power of Node.js.