Write a Simple npx Business Card
Best practices and patterns for publishing any node CLI

Write a Simple npx Business Card

Best practices and patterns for publishing any node CLI

One of the most common things we do as software engineers is scratch our itches. We see a pattern of repetition, and we seek out a utility to reduce or eliminate it. When a solution does not already exist, we can craft one. When working in the terminal, this is often accomplished by creating an alias or writing a small bash script. Lately, I prefer to write my CLI utilities using node.

javascript is awesome for CLIs

  • the npm build, publish, share cycle is so easy
  • npx allows execution without installation
  • npm ecosystem is ripe for CLI wrappers, just write the stdio handler for your favorite package

npx business card

Let’s create a pointless CLI just to walk thru some best practices and patterns for publishing any node CLI.

Inspired by johnkpaul and searls, we’ll create a npx enabled business card. The final output might look something like this:

npm init

Start a new node project and name it whatever you want. You could choose to name it after the executable you want to expose. It’s not necessary but conventions are nice, and it makes your binary more npx friendly.

mkdir jackboberg
cd jackboberg
npm init -y

At first, let’s get the necessary CLI working.

touch cli.js
chmod +x cli.js

Now open the new file in your favorite editor. I’ve been using spacemacs recently, but am still very partial to vim.

#!/usr/bin/env node

console.log('doing business')

You can execute your new CLI like this:

./cli.js

ship it

That’s an entirely functional first release! You just need to modify your package.json to let npm know to link your executable and you are off:

{
  // ...
  "bin": "./cli.js",
  // ....
}

By default, npm will expose your binary using the same name as your package. You can configure this if it’s not what you want, but it will make your executable harder to use with npx.

Share your new CLI with the world!

npm version patch
npm publish

Now anyone can leverage your new CLI using npx, without needing to install it locally:

npx $YOUR_PACKAGE_NAME

Put your logic in a separate file

What if you want to tell people more about the business you are doing. Create an object to hold the data for our CLI. I like to keep even small projects organized, so I’ll create a new file in lib for this.

# ./lib/data.js
module.exports = {
  name: 'Jack Boberg',
  title: 'Software Engineer',
  github: 'jackboberg',
  urls: [
    'https://jackboberg.info',
    'https://studioelsa.se'
  ]
}

When your CLI needs to do additional work, put that code in index.js or a local module. Consumers will be able to use your package programmatically, and it gives you a discrete location to do all your stateful business logic. In this case, we will use the main module to format our data as a pretty string.

Let’s get a library that will help make our output a bit more stylish.

npm i prettyjson

We can use this package, or any other useful utility we find on npm, to improve our CLI utility.

# ./index.js
const { render } = require('prettyjson')
const data = require('./lib/data')

const renderOpts = {
  dashColor: 'cyan',
  keysColor: 'blue',
  stringColor: 'white'
}

module.exports = () => render(data, renderOpts)

A small update to our ./cli.js script and we can leverage our new module.

#!/usr/bin/env node

const pkg = require('..')
console.log(pkg())

Handle stdio in your CLI module explicitly

That’s not bad, but what if someone wants to get the raw JSON. Let’s add a simple flag to modify our scripts behavior.

npx jackboberg -j

Keeping your cli.js focused on parsing input and producing output is a good idea; it keeps your files small and reasonable. Resist the urge to write any business logic in this file.

#!/usr/bin/env node

const minimist = require('minimist')
const pkg = require('.')

const options = {
  alias: { json: 'j' }
}
const argv = minimist(process.argv.slice(2), options)

console.log(pkg(argv))

prettyjson already includes minimist as a dependency, so you can add it for ‘free’ and dress up your CLI from there. You should still include it explicitly in dependencies. Let’s make a small change to our main script to support this new option.

# ./index.js

// ...

module.exports = ({ json }) => json ? JSON.stringify(data) : render(data, renderOpts)

Don’t forget to publish your final npx business card when you are finished adding tons of unnecessary additional features. I made mine include cowsay, because of reasons.

npm version major
npm publish

Now you have a globally executable script, that can leverage all of npm, to perform awesome utilites (or silly ideas like this). If you find you are writing a ‘big’ CLI with lots of parameters or sub-commands, you will want more options around how to define your CLI and should consider trying yargs instead of minimist.


Get this sent to your inbox

Let us share our thinking, opinions, practices, valuable resources and everything else that is happening at Studio Elsa.