Skip to content
Node.js nd libraries 5 min read

Building CLIs with Commander & Yargs

Command-line tools are a staple of the Node.js ecosystem — from scaffolding generators to deployment scripts and developer utilities. Parsing process.argv by hand quickly turns into a mess of string-splitting and if statements, so the community settled on two mature libraries: Commander and Yargs. Both turn raw arguments into structured commands, options, and flags, and both generate polished --help output for free. This page shows how to define commands, parse arguments, build subcommands, and ship a professional CLI.

Why a parsing library

A CLI needs to understand three kinds of input: commands (the verb, like git commit), arguments (positional values), and options/flags (named modifiers like --verbose or -o file.txt). A parser maps these to a clean JavaScript object, validates required values, applies defaults, coerces types, and prints usage text when the user makes a mistake. Commander leans toward a fluent, declarative API; Yargs favors a chainable builder with rich validation and middleware. Both are battle-tested and ship with TypeScript types.

FeatureCommanderYargs
API styleFluent / declarativeChainable builder
Subcommands.command().command() with builder/handler
Auto helpYesYes
Type coercionManual / customBuilt-in (number, boolean, array)
ValidationVia custom code.demandOption, .choices, .check
Bundle sizeSmallerLarger (more features)

Setting up the executable

Both libraries are installed from npm and run from a file marked as executable. The shebang line lets the OS run the script directly, and the bin field in package.json maps a command name to it.

npm install commander yargs
{
  "name": "taskcli",
  "version": "1.0.0",
  "type": "module",
  "bin": { "task": "./bin/task.js" }
}

After npm link (or installing globally), the task command is available on your PATH.

Commander

Commander uses a program object that you configure with a name, version, and a series of commands. Options are declared with short and long flags, and the parsed values are read from program.opts() or passed directly to action handlers.

#!/usr/bin/env node
import { Command } from "commander";

const program = new Command();

program
  .name("task")
  .description("A tiny task manager CLI")
  .version("1.0.0");

program
  .command("add")
  .description("Add a new task")
  .argument("<title>", "task title")
  .option("-p, --priority <level>", "priority level", "normal")
  .option("--done", "mark as already completed", false)
  .action((title, options) => {
    console.log(`Added "${title}" [priority=${options.priority}]`);
    if (options.done) console.log("Marked as done.");
  });

program
  .command("list")
  .description("List tasks")
  .option("-a, --all", "include completed tasks")
  .action((options) => {
    console.log(options.all ? "Listing all tasks" : "Listing open tasks");
  });

program.parse();

Running the tool produces clean, predictable output.

task add "Write docs" --priority high --done

Output:

Added "Write docs" [priority=high]
Marked as done.

In the option spec, <level> means a value is required, while [level] would make it optional. Commander auto-generates help when you pass --help or use an unknown command:

task --help

Output:

Usage: task [options] [command]

A tiny task manager CLI

Options:
  -V, --version   output the version number
  -h, --help      display help for command

Commands:
  add <title>     Add a new task
  list            List tasks
  help [command]  display help for command

Tip: Commander parses options after the command by default. Use program.enablePositionalOptions() if you need global options to coexist with subcommand options that share a name.

Yargs

Yargs builds the parser through chained calls. Each command() takes a name string (with positional placeholders), a description, a builder that declares options, and a handler that runs the logic. Yargs coerces types automatically and offers declarative validation.

#!/usr/bin/env node
import yargs from "yargs";
import { hideBin } from "yargs/helpers";

yargs(hideBin(process.argv))
  .scriptName("task")
  .command(
    "add <title>",
    "Add a new task",
    (y) =>
      y
        .positional("title", { type: "string", describe: "task title" })
        .option("priority", {
          alias: "p",
          choices: ["low", "normal", "high"],
          default: "normal",
        })
        .option("done", { type: "boolean", default: false }),
    (argv) => {
      console.log(`Added "${argv.title}" [priority=${argv.priority}]`);
      if (argv.done) console.log("Marked as done.");
    }
  )
  .command(
    "list",
    "List tasks",
    (y) => y.option("all", { alias: "a", type: "boolean" }),
    (argv) => {
      console.log(argv.all ? "Listing all tasks" : "Listing open tasks");
    }
  )
  .demandCommand(1, "You must provide a command.")
  .strict()
  .help()
  .parse();

The hideBin helper strips node and the script path from process.argv. .demandCommand(1) forces at least one command, .strict() rejects unknown flags, and choices restricts a value to a fixed set.

task add "Ship release" -p urgent

Output:

Invalid values:
  Argument: priority, Given: "urgent", Choices: "low", "normal", "high"

Yargs caught the invalid value before the handler ran — no manual checks needed. With CommonJS, replace the imports with const yargs = require("yargs/yargs") and const { hideBin } = require("yargs/helpers").

Subcommands and nesting

Real tools group related actions (task config set, task config get). Commander nests commands by adding child command() calls to a parent, while Yargs nests by calling .command() inside a builder.

// Commander nested subcommands
const config = program.command("config").description("Manage settings");
config
  .command("set <key> <value>")
  .action((key, value) => console.log(`Set ${key} = ${value}`));
config
  .command("get <key>")
  .action((key) => console.log(`Value of ${key}`));

Polishing the experience

A professional CLI does more than parse. Pair these libraries with chalk for colored output, ora for spinners, and @inquirer/prompts for interactive questions. Always set a non-zero exit code on failure so scripts and CI pipelines can detect errors.

program
  .command("deploy")
  .action(async () => {
    try {
      await runDeploy();
      console.log("Deploy succeeded.");
    } catch (err) {
      console.error(`Deploy failed: ${err.message}`);
      process.exitCode = 1;
    }
  });

Best practices

  • Add a shebang (#!/usr/bin/env node) and a bin entry in package.json so the tool installs as a real command.
  • Use required (<value>) versus optional ([value]) placeholders deliberately, and provide sensible defaults for options.
  • Validate input early — use Yargs choices/demandOption or Commander custom argument parsers rather than checking inside handlers.
  • Set process.exitCode = 1 (or call process.exit(1)) on errors so CI and shell scripts react correctly.
  • Keep handlers thin: parse arguments in the CLI layer, then call plain functions that are easy to unit test.
  • Write descriptions for every command and option — they power the auto-generated --help, which is your tool’s documentation.
  • Enable .strict() in Yargs (or validate unknown options in Commander) to catch typos instead of silently ignoring them.
Last updated June 14, 2026
Was this helpful?