Commander is the most popular framework for building command-line tools in Node — used by Vue CLI, create-react-app, and countless others. Where util.parseArgs just parses, Commander gives you a full CLI: declarative options and arguments with types and defaults, subcommands (git commit-style), automatically generated --help and --version, validation, and clean error messages. You describe your command's shape and Commander handles parsing, help, and dispatch. This page covers options and arguments, subcommands, the action handler, auto-help, and validation.
A first command
Bash
npm install commander
JS
#!/usr/bin/env node
import { program } from 'commander'
program
.name('greet')
.description('A friendly greeting CLI')
.version('1.0.0') // wires up --version / -V automatically
program
.argument('<name>', 'who to greet') // <required> vs [optional]
.option('-l, --loud', 'shout the greeting') // boolean flag
.option('-r, --repeat <n>', 'times to repeat', '1') // option with value + default
.action((name, options) => {
let msg = `Hello, ${name}!`
if (options.loud) msg = msg.toUpperCase()
for (let i = 0; i < Number(options.repeat); i++) console.log(msg)
})
program.parse() // parse process.argv and run
$ greet Ada --loud --repeat 2
HELLO, ADA!
HELLO, ADA!
You declare the command's name, version, arguments, and options — Commander parses, validates, and calls your `.action()`
Commander is **declarative**: you describe the command and it does the work. `.name()`, `.description()`, and `.version()` set metadata (`.version()` automatically creates the `--version`/`-V` flag). `.argument('<name>')` declares a positional — angle brackets `<>` mark it **required**, square brackets `[]` **optional**. `.option()` declares flags and valued options, taking the short/long form, a description, and an optional default. Finally `.action(handler)` registers the function that runs, receiving the parsed arguments followed by an `options` object. `program.parse()` reads `process.argv`, validates everything against your declarations, dispatches to the action — and on bad input prints a helpful error and exits non-zero, all without you writing parsing or help code.
Subcommands — git-style CLIs
JS
program
.command('init') // mytool init
.description('scaffold a new project')
.option('--template <name>', 'starter template', 'default')
.action((options) => scaffold(options.template))
program
.command('build') // mytool build <entry>
.description('build the project')
.argument('<entry>', 'entry file')
.option('-w, --watch', 'rebuild on change')
.action((entry, options) => build(entry, { watch: options.watch }))
program.parse()
// → mytool --help lists both commands; mytool build --help shows build's options
`.command()` defines subcommands, each with its own arguments, options, and action — like `git commit` / `git push`
Complex tools are organized as **subcommands** — `git commit`, `npm install`, `docker run` — and `.command('name')` is how you build them. Each subcommand is its own mini-program with independent `.argument()`s, `.option()`s, and `.action()`, so `mytool init` and `mytool build` parse and behave separately. Commander routes the user's first token to the matching command and dispatches to its action. Help is hierarchical for free: `mytool --help` lists all commands with their descriptions, and `mytool build --help` shows just that subcommand's options. For very large CLIs you can even split each command into its own file (or a standalone executable) and register them, keeping the codebase organized as the tool grows.
Auto-generated help and version
$ greet --help
Usage: greet [options] <name>
A friendly greeting CLI
Arguments:
name who to greet
Options:
-V, --version output the version number
-l, --loud shout the greeting
-r, --repeat <n> times to repeat (default: "1")
-h, --help display help for command
`--help` and `--version` are generated from your declarations — no manual usage strings to write and keep in sync
Because you've already declared every argument and option with a description, Commander **generates `--help` automatically** — formatted usage, the argument list, every option with its default, and the standard `-h`/`--help` and `-V`/`--version` entries. This is a real maintenance win: hand-written usage text inevitably drifts out of sync with the actual flags, but Commander's help is derived from the single source of truth, so it's always accurate. You can enrich it with `.addHelpText()` for examples, and customize program/command descriptions. Users get the `--help` they expect from every CLI, and you never write or update a usage string by hand.
Validation and custom processing
JS
program
// Coerce + validate an option value with a parser function:
.option('-p, --port <number>', 'port to listen on', (value) => {
const port = Number(value)
if (Number.isNaN(port) || port < 1 || port > 65535) {
throw new commander.InvalidArgumentError('Port must be 1–65535.')
}
return port // stored already-coerced to a Number
}, 3000)
// Restrict to a fixed set of choices:
.addOption(new commander.Option('--mode <mode>', 'run mode').choices(['dev', 'prod']))
.action((options) => start(options.port, options.mode))
Validate and coerce option values at parse time — bad input should fail fast with a clear message, not deep in your code
Commander lets you **validate and transform** values where they're declared, so bad input is rejected immediately with a friendly message rather than blowing up somewhere deep in your logic. Pass a **custom processing function** as the option's parser to coerce (string → `Number`) and validate, throwing `InvalidArgumentError` with a clear message on failure — Commander catches it, prints the error, and exits non-zero. Use `.choices([...])` to restrict an option to a fixed set, and `<value>` (required) vs `[value]` (optional) on arguments to enforce presence. Catching errors at the boundary like this is what makes a CLI feel polished: the user learns *exactly* what they got wrong, at the moment they ran the command, instead of getting a stack trace. Pair this with sensible [exit codes](/nodejs/cli-intro) and your tool integrates cleanly into scripts.
Commander vs the alternatives
Tool
Style
Best for
Commander
Declarative, chainable, lightweight
Most CLIs — the popular default
yargs
Declarative, very feature-rich
Complex parsing, middleware, advanced help
oclif
Class-based, plugin framework
Large multi-command CLIs (Heroku, Salesforce)
util.parseArgs
Built-in, parse-only
Simple tools, zero dependencies
Commander is the right default; yargs for heavy parsing, oclif for large plugin-based CLIs, parseArgs for tiny tools
All of these build Node CLIs; the choice is about scale and style. **Commander** hits the sweet spot for the vast majority of tools — small, declarative, well-documented, with everything (subcommands, help, validation) you usually need. **yargs** is similarly capable with an even richer parsing/middleware feature set, favored when you need its advanced behaviors. **oclif** is a heavier, class- and plugin-based *framework* built for large, extensible CLIs like Heroku's and Salesforce's, with generators and a plugin ecosystem. And [`util.parseArgs`](/nodejs/parsing-args) remains the no-dependency pick for trivial tools. Default to Commander, reach for oclif only when you're building something CLI-as-a-platform sized.
Next
Make your CLI interactive with prompts and menus: [Interactive Prompts (Inquirer)](/nodejs/inquirer).