How to Create a Real-World CLI App with Node.js

As a developer, you likely spend a lot of time in the terminal. While GUIs are great for many tasks, the command line really lets you fly. It‘s a big productivity booster to be able to accomplish tasks with just a few keystrokes.

Command line interfaces (CLIs) aren‘t just for using existing tools – you can also build your own! A well-designed CLI can be a great addition to your own projects. It could be an easy way for users to interact with your library or quickly accomplish a set of tasks.

In this tutorial, you‘ll learn how to build a real-world CLI app using Node.js. We‘ll create a tool that lets you check the weather forecast right in your terminal. Along the way, you‘ll pick up best practices for building Node CLIs and get ideas for further enhancing the app.

Why Build CLIs with Node?

You have a lot of options when it comes to building command line apps. Just about any programming language can be used to write scripts that run in the terminal. So why choose Node.js?

First off, JavaScript is a very accessible and popular language. If you‘re already using Node for web development, you‘ll be able to transfer those skills directly to building CLI apps.

Node also has a huge ecosystem of open source packages in npm. Virtually any common CLI task – from parsing arguments to displaying spinners – has a mature and well-tested library available. You can focus on the core features of your app without reinventing the wheel.

But the biggest reason to use Node is that you can share code between your CLI and other JavaScript interfaces. For example, if you have a web app and want to make some functionality available via a CLI, you could abstract the core logic into a shared library imported by both codebases. This makes your code more maintainable and keeps your CLI and other UIs in sync.

Project Setup

Let‘s jump into building our weather CLI. We‘ll assume you have a recent version of Node and npm installed. Create a new directory for the project and run npm init -y inside it to initialize a package.json file.

Next, create a folder called bin with a file called weather-cli inside. This will be our entry point for the CLI app. Put the following code in that file:

#!/usr/bin/env node
require(‘../‘)()

The first line is called a "shebang." It tells the system to run this file with the node executable. The second line requires our main app file (which we‘ll create momentarily) and immediately invokes it.

To get this script set up as a global command line tool, open up your package.json and add this:

"bin": {
  "weather": "bin/weather-cli"
},

The key is the command name and the value is the path to the script. Now create an index.js file in the root of the project with this starter code:

module.exports = () => {
  console.log(‘Welcome to the weather CLI!‘)
}

Back in the terminal, run:

npm link

This installs the package globally and symlinks the weather command to the entry point script. Now from anywhere on your machine, you should be able to run weather and see the welcome message. Our CLI is up and running!

Parsing Arguments

To make our simple script actually useful, we need to be able to pass in arguments. Say we want to be able specify a location and get the current weather. We could provide the location as a command-line argument like weather boston.

We could manually parse process.argv to get the value of the argument, but there‘s a better way. Install the minimist library:

npm install minimist

Now update index.js:

const minimist = require(‘minimist‘)

module.exports = () => {
  const args = minimist(process.argv.slice(2))
  console.log(args)
}

We pass minimist everything after the script name in process.argv. Run weather boston again and you should see { _: [‘boston‘] } logged out. Minimist has parsed the location argument for us.

Let‘s add a couple more arguments. Update the code to look like this:

const minimist = require(‘minimist‘)

module.exports = () => {  
  const args = minimist(process.argv.slice(2), {
    alias: { h: ‘help‘, v: ‘version‘ },    
  })

  if (args.version) {
    console.log(‘1.0.0‘)
  } else if (args.help) {
    printHelp()
  } else {
    const location = args._[0]
    console.log(`Getting weather for ${location}`)
  }
}

function printHelp() {
  console.log(‘usage: weather <location> [options]‘)  
  console.log(‘‘)
  console.log(‘options:‘)
  console.log(‘  -v, --version   print version‘)
  console.log(‘  -h, --help      print help menu‘)
}

We‘ve added two special arguments – version and help. If either of those flags are provided, we print out the version number or help text respectively. Otherwise, we grab the location from the first "bare" argument in the _ array.

Try it out:

weather boston
Getting weather for boston

weather --version  
1.0.0

weather --help
usage: weather <location> [options]

options:
  -v, --version   print version 
  -h, --help      print help menu

weather
Getting weather for undefined

Minimist gives us an easy way to define aliases and parse different types of arguments. Of course there‘s a lot more customization you can do, so check out the readme for more options.

Getting Weather Data

Now that we can get the location from the CLI arguments, let‘s use it to fetch some real weather data. There are quite a few weather APIs we could use, but I like OpenWeatherMap. It has a generous free plan and doesn‘t require any API key for basic requests.

Install the axios HTTP client:

npm install axios  

And add this function to index.js:

const axios = require(‘axios‘)

async function getWeather(location) {
  const results = await axios({
    method: ‘get‘,
    url: ‘https://api.openweathermap.org/data/2.5/weather‘,
    params: {
      q: location,
      units: ‘imperial‘,
      appid: ‘YOUR_API_KEY‘
    }
  })

  return results.data
}

Make sure to replace YOUR_API_KEY with your actual OpenWeatherMap API key.

Back in our main function, we can now call this getWeather function with the location argument:

const location = args._[0]

if (!location) {
  return printHelp()
}

console.log(`Getting weather for ${location}...`)

getWeather(location)
  .then(data => {
    const temperature = data.main.temp
    const description = data.weather[0].description
    const city = data.name
    const country = data.sys.country

    console.log(`Current conditions in ${city}, ${country}:`)  
    console.log(`  ${temperature}°F and ${description}`)
  })
  .catch(err => console.log(`Error getting weather for ${location}:`, err.message))

First we check if a location was provided, and show the help text if not. Assuming we have a location, we call our getWeather function and wait for the promise to resolve. The response from OpenWeatherMap includes the current temperature, a basic description of the conditions, and the city/country.

We format that data into a nice message to print out in the console. We also catch any errors and display a message. Here‘s what we get now:

weather boston  
Getting weather for boston...
Current conditions in Boston, US:  
  73.4°F and few clouds

Looking good! We‘re successfully fetching live weather data in our CLI.

Adding a Forecast

Let‘s add one more command to display a 5-day forecast for the given location. OpenWeatherMap has an endpoint for this, so we just need to add another API call.

In index.js:

async function getForecast(location) {
  const results = await axios({
    method: ‘get‘,
    url: ‘https://api.openweathermap.org/data/2.5/forecast‘,
    params: {
      q: location,
      units: ‘imperial‘,
      cnt: 40,
      appid: ‘YOUR_API_KEY‘
    }  
  })

  return results.data
}

And let‘s create a separate file for this new "forecast" command. Make a new file called forecast.js:

const ora = require(‘ora‘)
const getWeather = require(‘../utils/weather‘)

module.exports = async (args) => {
  const spinner = ora().start()

  try {
    const location = args._[0]
    const data = await getForecast(location)

    spinner.stop()

    console.log(`Forecast for ${data.city.name}, ${data.city.country}:`)

    data.list.forEach(item => {
      console.log(`${item.dt_txt}:`)
      console.log(`  ${item.main.temp}°F with ${item.weather[0].description}`) 
    })
  } catch (err) {
    spinner.stop()
    console.error(err)
  }
}

This follows a similar pattern to the current weather command. We get the location argument, call the getForecast function, await the results, and output them to the console.

To load this command from our main CLI, add a switch statement in index.js:

const arg = args._[0]

switch (arg) {
  case ‘current‘:
    // print current weather
    break
  case ‘forecast‘:
    require(‘./cmds/forecast‘)(args)  
    break
  default:
    console.error(`"${arg}" is not a valid command!`)
    break
}  

And update the help text to mention the forecast command:

function printHelp() {
  console.log(‘usage: weather <command> [options]‘)
  console.log(‘‘)  
  console.log(‘commands:‘)
  console.log(‘  current <location>   get current weather conditions‘)
  console.log(‘  forecast <location>  get 5-day forecast‘)
  console.log(‘‘)
  console.log(‘options:‘)
  console.log(‘  -v, --version        print version‘)
  console.log(‘  -h, --help           print help menu‘)  
}

Let‘s take it for a spin:

weather current boston
Current conditions in Boston, US:
  73.4°F and few clouds

weather forecast london  
Forecast for London, GB:  
2020-08-24 18:00:00:
  71.06°F with few clouds
2020-08-24 21:00:00:
  68.92°F with few clouds
2020-08-25 00:00:00:
  66.22°F with broken clouds
...

Excellent! We now have an easy way to check the current weather and 5-day forecast right from the command line.

Bonus: Loading Indicator

For a bit more polish, let‘s add a loading indicator while we‘re fetching the weather data. Install the ora package:

npm install ora

And add it to the current weather command:

const ora = require(‘ora‘)

// ...

const spinner = ora().start()

getWeather(location)  
  .then(data => {
    spinner.stop()

    // ...
  })
  .catch(err => {
    spinner.stop()
    console.log(`Error getting weather for ${location}:`, err.message)  
  })

Ora gives us a nice spinner that automatically starts and stops. This reassures the user that something is happening while the API call is being made. The forecast command already includes this loading indicator as well.

Publishing & Sharing

The last step is to get our CLI tool out into the world! We can package this up and publish it to npm.

First, make sure your package.json file has the right info:

{
  "name": "weather-cli",
  "version": "1.0.0",
  "description": "Check weather in your terminal",
  "license": "MIT", 
  "homepage": "https://github.com/yourusername/weather-cli",
  "repository": {
    "type": "git",
    "url": "https://github.com/yourusername/weather-cli.git"
  },
  "bin": {
    "weather": "bin/weather-cli"  
  },
  "engines": {
    "node": ">=10"
  },
  "scripts": {
    "start": "node ."
  },
  "keywords": [
    "weather",
    "forecast",
    "cli"
  ]
}

Replace "yourusername" with your actual GitHub username. The "bin" field is crucial – it tells npm where your executable script lives.

Also add a README.md file that describes how to install and use your CLI. Here‘s a template to get you started:

# weather-cli

Check the weather right in your terminal!

## Installation  
Install globally:

npm install -g weather-cli


## Usage

Check current weather conditions:

weather current


Get a 5-day forecast:  

weather forecast


## Options

-v, –version print version
-h, –help print help menu

Fill in your specific usage details and feel free to expand on the readme.

Finally, log into npm (npm login), then publish:

npm publish

That‘s it! Your CLI is now available for anyone to install and use via npm. You can share the link to the npm package and the CLI will automatically be installed when someone runs npm install -g your-package.

Further Reading

We‘ve built a super useful real-world CLI using Node! There‘s a ton more you can do to extend this app:

  • Pull in ASCII art to display weather icons
  • Add colorized output for hot/cold temperatures
  • Allow the user to configure default location and units
  • Provide an interactive prompt to input location
  • Display weather maps or other graphics with a package like blessed

I‘ll leave those fun features for you to implement. Hopefully this tutorial has shown you how powerful Node CLIs can be and how they let you quickly build interactive tools. Now go make some awesome command-line apps!

Similar Posts