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.
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?
- Automate tasks
- Create tools for developers
- Interact with systems and manage flows
Real World Examples
At Nielsen we created several CLIs that provided 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.
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 type— Asks 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!