Building a CLI App with Node.js in 2024

An in-depth step-by-step guide to creating a CLI App with Node.js, covering everything from command handling and user prompts to enhancing user experience, as well as organizing project structure and documentation.

Evgeni Gomziakov
Nielsen-TLV-Tech-Blog
6 min readFeb 9, 2024

--

Why Node?

The event-driven architecture, along with the npm ecosystem that offers many packages designed specifically for this purpose, makes it the go-to choice for developing efficient and scalable CLI tools.

Why Build a CLI App?

  1. Automate tasks
  2. Create tools for developers
  3. Interact with systems and manage flows

Real World Examples

At Nielsen we created several CLIs that provide huge value:

  • A CLI that manages dynamic pipelines in the CI/CD flows — no more manual configuration or waiting between processes.
  • A CLI that sets up and manages local dockerized development environments.
  • A CLI that runs predefined steps for migrations.

Now you’ll see how easy it is to make one.
For those eager to dive straight into the code, the files can be found here.

Setting Up

make sure you’ve got Node.js on your machine.

Step 1: Bootstrap Your Project

Create a new folder for your project and jump into it:

mkdir my-node-cli
cd my-node-cli

Fire up a new Node.js project:

npm init

Step 2: Bring in Commander.js

Commander.js is our go-to for building CLIs in Node.js. It’s like having a swiss army knife for parsing input, help text, and managing errors.

npm install commander

Step 3: Crafting The CLI

Create a file named index.js in your project folder. This will be where our CLI starts. Add a shebang at the top to get this CLI off the ground.

#!/usr/bin/env node

import { program } from "commander";

program
.version("1.0.0")
.description("My Node CLI")
.option("-n, --name <type>", "Add your name")
.action((options) => {
console.log(`Hey, ${options.name}!`);
});

program.parse(process.argv);

Add bin to your package.json to recognize your CLI command, and type to work with ES modules instead of CommonJS:

"bin": {
"my-node-cli": "./index.js"
},
"type": "module"

Link your project globally with:

npm link 

And just like that, my-node-cli is ready to run on your terminal!

my-node-cli --name YourName

Note: since Node.js 18.3 we have built in command-line arguments parser. You can read about it here and decide if you want to use it instead of commander.js.

User Experience

Add Some Color

Chalk is perfect for making your CLI’s output pop with color. Grab it with:

npm install chalk

Now, let’s improve our greeting:

#!/usr/bin/env node

import { program } from "commander";
import chalk from "chalk";

program
.version("1.0.0")
.description("My Node CLI")
.option("-n, --name <type>", "Add your name")
.action((options) => {
console.log(chalk.blue(`Hey, ${options.name}!`));
console.log(chalk.green(`Hey, ${options.name}!`));
console.log(chalk.red(`Hey, ${options.name}!`));
});

program.parse(process.argv);

Prompting Made Easy

For a more interactive vibe, Inquirer.js is your friend.

npm install inquirer

Instead of using command line options to collect data — just ask the user

#!/usr/bin/env node

import { program } from "commander";
import chalk from "chalk";
import inquirer from "inquirer";

program.version("1.0.0").description("My Node CLI");

program.action(() => {
inquirer
.prompt([
{
type: "input",
name: "name",
message: "What's your name?",
},
])
.then((answers) => {
console.log(chalk.green(`Hey there, ${answers.name}!`));
});
});

program.parse(process.argv);

There is a Confirm prompt typeAsks the user a yes/no question.

List prompt type — Allows the user to choose from a list of options.

And there are also Checkbox, Password, Rawlist and Expand. Feel free to explore more at https://github.com/SBoudrias/Inquirer.js

Cool Loaders

Loading times? Make them fun with ora. It’s great for adding spinner animations:

npm install ora

Sprinkle in a loader for processes that take time:

#!/usr/bin/env node

import { program } from "commander";
import chalk from "chalk";
import inquirer from "inquirer";
import ora from "ora";

program.version("1.0.0").description("My Node CLI");

program.action(() => {
inquirer
.prompt([
{
type: "list",
name: "choice",
message: "Choose an option:",
choices: ["Option 1", "Option 2", "Option 3"],
},
])
.then((result) => {
const spinner = ora(`Doing ${result.choice}...`).start(); // Start the spinner

setTimeout(() => {
spinner.succeed(chalk.green("Done!"));
}, 3000);
});
});

program.parse(process.argv);

Adding ASCII Art

Let’s add some final touches with figlet.js:

npm install figlet

Add this to your index.js

import figlet from "figlet";

console.log(
chalk.yellow(figlet.textSync("My Node CLI", { horizontalLayout: "full" }))
);

There are various fonts and customization options, allowing you to tailor the ASCII art to your CLI’ aesthetic.

Project Structure

Keeping things organized can save you a ton of time later on, especially as your project grows. Here’s a simple yet effective structure to start with:

my-node-cli/
├─ bin/
│ └─ index.js
├─ src/
│ ├─ commands/
│ ├─ utils/
│ └─ lib/
├─ package.json
└─ README.md
  • bin — is where your CLI’s lives. It’s what gets called when someone runs your CLI.
  • src/commands — holds individual command files. This makes adding new commands or editing existing ones cleaner.
  • src/utils — is for utility functions you might need across several commands, like formatting data.
  • src/lib — could be where your core functionality resides, especially if your CLI interacts with APIs or performs complex logic.

Documentation

Clear documentation is key. Outline installation, usage, and command options in your README.md to guide users through your CLI tool.

# My Node CLI
My Node CLI is a tool for doing awesome things directly from your terminal.

## Installation

```bash
npm install -g my-node-cli
```

## Usage
To start using My Node CLI, run:

```bash
my-node-cli - help
```

### Commands
- `my-node-cli - name YourName`: Greets you by your name.
- `my-node-cli option1`: Executes option 1.

For more detailed information on commands, run `my-node-cli --help`.

## Contributing
Contributions are welcome ...

## License
This project is licensed ...

Auto Generating Documentation

Consider using tools like JSDoc or TypeDoc, they can generate detailed documentation from your code comments.

/**
* This function greets the user by name.
* @param {string} name The name of the user.
*/
const greet = (name) => {
console.log(`Hello, ${name}!`);
};

Best Practices

Before you start working on the actual CLI logic I would strongly recommend checking this repo by Liran Tal, it has more than 3k stars and covers all the best practices I could think of and more.

For example, instead of requiring your user to repeatedly provide the same information between invocation, provide a stateful experience using conf, for saving data like username, email or API tokens.

Ready to see all this in action? Check out the complete project along with all the example files on my GitHub page. Dive in, play around, and don’t hesitate to fork or star the repo if you find it helpful!

--

--