The CLI Book: Writing successful Command Line Clients with Node.js

Learn how to write successful Command Line Clients with Node.js


Become a successful creator

The CLI Book is your step-by-step guide to write Command Line Clients with Node.js. Learn everything what makes Command Line Clients successful and learn to compose the best Command Line Interfaces (CLI). The first part of the book explains what is needed for a successful Command Line Client, in the second part we build an application based on the learned principles. You'll get everything you need to make an impact in the areas you care about with Command Line Clients.


Command line clients are everywhere. Almost everyone, at least in tech, is using them.

There are a lot of successful command line clients out there: the Linux project has git and the Node.js project has npm. We use some of them multiple times per day. Apache CouchDB recently got nmo (speak: nemo), a tool to manage the database cluster. We can learn a lot from successful command line interfaces in order to write better command line clients.

When I started to get interested in command line clients I realised that there are a lot of discussions and informations on the web about writing APIs. The web is full of tutorials to teach you how to build APIs, especially REST-APIs, but almost nothing can be found about writing good CLIs. This book tries to explain what makes a good CLI. In the second part of the book we will build a small command line client to learn how to use Node.js to create great command line clients that people love.

The goal of the book is to show the principles to build a successful command line client. The provided code should give you a good understanding what is important to build successful command line clients and how you could implement them.

Every section has its own code examples. Before you run the code, you have to run npm install in the folder that belongs to the section.

I am very happy about feedback. Please send and feedback or corrections to You can also contact me on twitter: @robinson_k

I hope you enjoy the book – please recommend it in case you like it.

What makes a good CLI?

In this chapter we will take a look at successful command line clients and what they are doing pretty well, which will help us to understand the problems users face using the Terminal. Understanding the problems of our users will help us to build better command line clients with Node later in the book.

Let’s take a look at how people usually use a CLI: most of the time a human sits in front of a keyboard and interacts with a terminal. We want to use simple and recognisable commands for our CLI. Sadly just easy recognisable commands don’t get us very far right now.

Maybe the problem is easier to understand if we take a look at something what I would call a bad CLI:

$ mycli -A -a 16 r foo.html
error: undefined is not a function

In my example I have to enter cryptic commands which is answered by a very cryptic error message. What does -A -a 16 and r mean? Why I am getting an error back, am I using it wrong? What does the error mean and how can I get my task done?

So what makes a good CLI? Let’s try it with the following three principles:

  • you never get stuck

  • it is simple and supports powerusers

  • you can use it for all the things!

In short: A successful CLI is successful because its users are successful and happy.

You never get stuck

Nobody likes to be in a traffic jam, stuck, just making a few meters per minute. We want to reach our target destination, that’s all we want! The same applies for our users. Both developers and users are extremely unhappy when the tools they use are standing in their way. They just want to get their task done.

So what does, „You never get stuck“ mean, exactly? It means that we should always offer our users a way to solve their task, a command should never be a dead end. Additionally the developers of the CLI should avoid every source of friction in their tool.

Let’s take a look at me, trying to use git:

$ git poll
git: 'poll' is not a git command. See 'git --help'.

Did you mean this?

In this example I entered a wrong command. git answers friendly: „Hey Robert, it looks like you entered a wrong command, but if you type in git --help, you can list all the existing commands. And hey, it just looks like you mistyped git pull, did you mean git pull?“

git offers us a way to continue our work and finish the task.

And if we take a look at npm, another successful CLI client, we’ll see the same concept:

$ npm ragrragr
Usage: npm <command>

where <command> is one of:
    access, add-user, adduser, apihelp, author, bin, bugs, c,
    cache, completion, config, ddp, dedupe, deprecate, dist-tag,
    dist-tags, docs, edit, explore, faq, find, find-dupes, get,
    help, help-search, home, i, info, init, install, issues, la,
    link, list, ll, ln, login, logout, ls, outdated, owner,
    pack, prefix, prune, publish, r, rb, rebuild, remove, repo,
    restart, rm, root, run-script, s, se, search, set, show,
    shrinkwrap, star, stars, start, stop, t, tag, test, tst, un,
    uninstall, unlink, unpublish, unstar, up, update, upgrade,
    v, verison, version, view, whoami

npm <cmd> -h     quick help on <cmd>
npm -l           display full usage info
npm faq          commonly asked questions
npm help <term>  search for help on <term>
npm help npm     involved overview

Specify configs in the ini-formatted file:
or on the command line via: npm <command> —key value
Config info can be viewed via: npm help config

npm@2.7.4 /Users/robert/.nvm/versions/node/v0.12.2/lib/node_modules/npm

In this example I try to put garbage into npm, so npm answers friendly: „Hey Robert, I don’t know that command, but here are all the commands that would be possible. You can use them like this and get help about them by typing in npm help <command>.“

Like git, npm immediately offers help to enable me to finish my task, even if I have no idea how to use npm at all.

Still lost?

What if I still need help? Maybe I want to get some help before I just try out commands. Turns out there is a quite reliable way to ship documentation on Unix or Linux, man-pages!

man git
Figure 1. The man-page for git pull

Man-pages are quite nice, as you don’t need the internet to open them. You can also stay in the same terminal window to read them and don’t have to switch to another window, e.g. a browser.

But some users don’t know about man-pages or they don’t like to use them. Additionally many of them will be on Windows which can’t handle man-pages natively, so git and npm offer their documentation as webpages, too:

web git
Figure 2. The documentation website of the git project

Both git and npm are making use of a trick: they write their documentation once (e.g. in Markdown or Asciidoc) and use the initital source as the base for the different formats of their docs. Later they convert them to different formats, e.g. to html.

If you take a look at the man-pages of git and npm, you will notice that their websites are basically framing the content from the man-page with a header and a sidebar.

man npm
Figure 3. The manpage for npm publish
web npm
Figure 4. The documentation website of npm

Error handling

Sometimes things go still horribly wrong… Let’s take a look at my example for a bad CLI again:

$ mycli -A -a 16 r
      throw er; // Unhandled 'error' event
Error: ENOENT, open 'cli.js'
    at Error (native)

In this case we are getting back a stacktrace without much context. For most people these stacktraces look quite cryptic, especially for people that don’t write Node.js on a daily basis.

And it is even worse: I really can’t tell if I just hit a bug in the command line client or if I am just using the CLI in a wrong way. Looking at that small terminal, with no idea what to do, I get extremely unhappy and so our users will get unhappy.

One thing nmo supports is „usage errors“ — here is what they look like:

$ nmo cluster dsf
ERR! Usage:

nmo cluster get [<clustername>], [<nodename>]
nmo cluster add <nodename>, <url>, <clustername>
nmo cluster join <clustername>

If a user tries to use a command in a wrong way, nmo will tell them immediately how they can use the command to get their job done. No need to open the documentation.

nmo also shows stacktraces to a user, if nmo crashes for serious reasons:

$ nmo cluster join anemone
ERR! df is not defined
ERR! ReferenceError: df is not defined
ERR!     at /Users/robert/apache/nmo/lib/cluster.js:84:5
ERR!     at cli (/Users/robert/apache/nmo/lib/cluster.js:68:27)
ERR!     at /Users/robert/apache/nmo/bin/nmo-cli.js:32:6
ERR! nmo: 1.0.1 node: v0.12.2
ERR! please open an issue including this log on

nmo adds the current nmo and node version to the stacktrace, like npm does. We also ask the user to copy the stacktrace and to open an issue containing the stacktrace.

The reports make it easy for the team to identify the bug, solve it, and release a new version of nmo by seeing the stacktrace.

And again the user is not stuck. The user gets help to solve their task, in the worst case we help them in our issue tracker.

It supports powerusers

Powerusers are important for your CLI. They are users who will talk about your CLI and raise the overall adoption by spreading the word.


Most power users are using your CLI multiple times every day. An easy way to support them is by providing shortcuts.

npm has lots of shortcuts. For instance, npm i is the short form for npm install. git lets you define your own shortcuts in the .gitconfig file. I use git co as a shortcut for git checkout, for example.


At some point your command line client will get quite successful, people are loving it and start using your CLI in creative ways. The command line client will suddenly run on a Jenkins, as part of their deployment in a chef or Puppet run, or your users will use it in ways you never could have imagined!

Sooner or later not only humans will use your CLI, but also automated processes. To make your CLI even more successful it’s a good idea to support scripting.

exit codes

Operating systems have different exit codes to signal if a command was successful. You will get back a 0 if your recent command was successful. 1 would be a general error.

Exit codes are very useful for users that want to wrap your command line client in a bash script.

Here is an example:

$ git poll
git: 'poll' is not a git command. See 'git --help'.

Did you mean this?
$ echo $?

git notifies me that something went wrong - I am getting back a 1 as exit code. With proper exit codes every writer of a bash script can handle the cases where a command is not successful.

JSON output

In nmo every command that gives back information supports json-formatted output:

$ nmo cluster get --json
{ anemone:
   { node1: 'http://node1.local',
     node2: 'http://node2.local',
     node3: 'http://node3.local' } }

JSON support enables user to process data easily in the programming language of their choice, as most languages support JSON. They spawn a child process in language x and listen to stdout for the output. They can also directly pipe the output into a consumer on the shell:

$ nmo cluster get --json |

JSON output gives a lot of flexibility to the users.

The API in the Command Line Client

There is another concept to make scripting easier: I call it the „The API in the Command Line Client“:

const nmo = require('nmo');

nmo.load({}).then(() => {
    .get('testcluster', 'node1@127.0.01')
    .then((res) => {

In nmo every command is exposed on nmo.commands. If a user wants to use nmo as part of their node scripts, they are able to require it. The JavaScript API is documented like the CLI.

The JavaScript API enables the users to embed nmo in their Node.js scripts for complex processes. They could even fork nmo and embed it into their own command line client.


Powerusers love configuration. Given they use a command line client a lot, maybe multiple times a day, it is no surprise that they would like to have some features enabled per default. But in rare cases, there is an exception and they don’t need the default setting.

npm supports option arguments on the command line:

$ npm i hapi --registry=
└── hapi@9.0.4

This command tries to download the package hapi from a private registry at

But I can also set this private registry as the new default registry:

$ npm config set registry

npm writes the new registry into the config:

$ cat ~/.npmrc

The next time I try to install a package, npm will use my new default registry,

$ npm i hapi

If I don’t want to use this new default registry I can pass an argument to the CLI and it will use the alternate registry just for this call:

$ npm i hapi --registry=
└── hapi@9.0.4

That means we have different priorities between default configurations and command line arguments in npm and this combination is extremely powerful.

You can use it for all the things!

Let’s take a look at the last principle, and the solution to it sounds very easy at first. Whenever I have to do a task multiple times and it fits into the domain of my command line client, I’ll just add it as a new command. This habit turns into a win-win situation: You have to do less boring tasks and your users get happy because they get a new feature and they also have to do less monkey tasks - making your command line client even more successful. Sadly it can be quite hard to spot common pain points, especially if you work with multiple teams and/or a lot different people. Additionally most of us are suffering organisational blindness working on the same topic after a certain time. But if you identify a task to automate for you and your users, you will be hugely rewarded!

Hi! Sorry to interrupt you!

I'm Robert Kowalski, the author of the book.

It seems you like the book, if you have read that far!

I'm currently working on a new version of The CLI Book. If you are interested about updates, drop me an email!

Table of Contents


What makes a good CLI?

    You never get stuck
    • Still lost?
    • Error handling
  • It supports powerusers
    • Shortcuts
    • Scripting
    • exit codes
    • JSON output
    • The API in the Command Line Client
    • Configuration
  • You can use it for all the things!

Writing a database administration tool with Node.js

  • Why use Node.js?
  • Setup
  • Using the PouchDB database server
  • Troubleshooting

A simple status check

  • Getting started from scratch
  • The internals of the command
  • The CLI part
  • Booting the tool
  • Error handling
  • JSON support and Shorthands
  • Documentation
  • More Help
  • Configuration
  • Our first release & release tips

Migration of large amounts of data using Streams

  • The first stream
  • The Transform and Writeable stream
  • The streaming import command

Tips & Tricks

  • Testing
  • Semantic Versioning with SemVer
  • Greenkeeper

About the Author

Hi! I am Robert Kowalski, a Software Engineer specialised on Open Source Software. I am a Node.js Core Committer and I worked on the npm client for several years, where I found my love for great Command Line Interfaces. I enjoy writing and lecturing about Node.js and Command Line Clients to inspire others and to share my knowledge.

Github | Twitter


Do you have questions about the book? I am happy to answer them! Just write me an email!

Jump to the packages