DEV Community

Orbit Websites
Orbit Websites

Posted on

Building CLI Tools with Node.js: A Practical Guide for Developers

Building CLI Tools with Node.js: A Practical Guide for Developers

You write JavaScript all day. You ship web apps, APIs, and microservices. But how often do you build tools that make your own life easier? CLI tools are one of the most underrated superpowers in a developer’s toolkit. With Node.js, you can turn repetitive tasks into fast, sharable commands — and it’s easier than you think.

This isn’t about theory. This is how to build real, usable CLI tools with Node.js — from zero to publish-ready.


1. Start with #!/usr/bin/env node

Every CLI tool needs a shebang. It tells the shell which interpreter to use. For Node.js, that’s:

#!/usr/bin/env node
Enter fullscreen mode Exit fullscreen mode

Put this at the top of your main script file (e.g., cli.js). Without it, your script won’t run as a command.

Example cli.js:

#!/usr/bin/env node

console.log('Hello from your CLI!');
Enter fullscreen mode Exit fullscreen mode

Now make it executable:

chmod +x cli.js
Enter fullscreen mode Exit fullscreen mode

Test it:

./cli.js
# Output: Hello from your CLI!
Enter fullscreen mode Exit fullscreen mode

This is your foundation. Everything else builds on top.


2. Make It Installable with npm

Want to run mytool instead of ./cli.js? Turn it into an npm package.

Create package.json:

{
  "name": "my-cli-tool",
  "version": "1.0.0",
  "bin": {
    "mytool": "./cli.js"
  },
  "preferGlobal": true
}
Enter fullscreen mode Exit fullscreen mode

The bin field maps the command name (mytool) to your script.

Now install it globally:

npm install -g .
Enter fullscreen mode Exit fullscreen mode

Now run:

mytool
# Output: Hello from your CLI!
Enter fullscreen mode Exit fullscreen mode

Boom. You’ve got a global command.

💡 Pro tip: Use npm link during development. It creates a symlink so you don’t have to reinstall after every change.


3. Parse Arguments Like a Pro

Raw process.argv is messy. Use a lightweight parser like minimist or yargs. I prefer yargs for anything beyond basic flags.

Install:

npm install yargs
Enter fullscreen mode Exit fullscreen mode

Update cli.js:

#!/usr/bin/env node

const yargs = require('yargs');

const argv = yargs
  .command('greet <name>', 'Greet someone', (yargs) => {
    yargs.positional('name', {
      describe: 'The person to greet',
      type: 'string'
    });
  }, (argv) => {
    console.log(`Hello, ${argv.name}!`);
  })
  .help()
  .argv;
Enter fullscreen mode Exit fullscreen mode

Now try:

mytool greet Alice
# Output: Hello, Alice!
Enter fullscreen mode Exit fullscreen mode

yargs gives you:

  • Auto-generated help (--help)
  • Type validation
  • Positional and optional args
  • Subcommands

It’s worth the few extra KB.


4. Handle Input/Output Gracefully

CLIs should be predictable. Don’t console.log() everything — structure your output.

For example, if your tool returns data, support --json:

// Inside your command handler
if (argv.json) {
  console.log(JSON.stringify({ message: `Hello, ${argv.name}!` }));
} else {
  console.log(`Hello, ${argv.name}!`);
}
Enter fullscreen mode Exit fullscreen mode

Also, use console.error for errors — keeps stdout clean for piping:

if (!argv.name) {
  console.error('Error: name is required');
  process.exit(1);
}
Enter fullscreen mode Exit fullscreen mode

And always exit with the right code: 0 for success, 1 (or higher) for failure.


5. Read from stdin When It Makes Sense

Good CLI tools play well with others. Support piping.

Example: let users pipe input into your tool.

#!/usr/bin/env node

let input = '';

process.stdin.setEncoding('utf8');

process.stdin.on('readable', () => {
  const chunk = process.stdin.read();
  if (chunk !== null) {
    input += chunk;
  }
});

process.stdin.on('end', () => {
  console.log(`You said: ${input.trim().toUpperCase()}`);
});
Enter fullscreen mode Exit fullscreen mode

Now test:

echo "hello world" | mytool
# Output: You said: HELLO WORLD
Enter fullscreen mode Exit fullscreen mode

This turns your tool into a Unix-style filter — composable and powerful.


6. Use Configuration Files (When Needed)

Don’t force users to pass the same flags every time. Support config files.

Use find-up and read-pkg-up or just fs + path to look for .mytoolrc in the current or parent dirs.

Quick example:

const fs = require('fs');
const path = require('path');

function loadConfig() {
  const configPath = path.resolve(process.cwd(), '.mytoolrc');
  if (fs.existsSync(configPath)) {
    return JSON.parse(fs.readFileSync(configPath, 'utf8'));
  }
  return {};
}
Enter fullscreen mode Exit fullscreen mode

Let users override defaults without cluttering their commands.


7. Add Autocomplete (Optional but Nice)

yargs supports bash/zsh autocomplete out of the box.

After installing your tool globally, run:


bash
mytool --

---

☕ 
Enter fullscreen mode Exit fullscreen mode

Top comments (0)