Daniel Romero

Weather CLI in Node.js

June 20, 2019

In this post we’re going to build a weather CLI again, this time we’ll be using Node.js though. I was curious as to how a minimal CLI in Node.js would compare against the one that we previously built using Python, so, here we go.

Setup

First, we need to create a new directory for the project wherever it is that we place our projects:

$ mkdir ~/projects/node-weather-cli
$ cd ~/projects/node-weather-cli

We use the npm init command to create and configure the package.json for our small utility while following the wizard:

$ npm init

Then we proceed to install the 3 dependencies that we’ll be using to build this:

$ npm install yargs axios moment

We create our main JavaScript file that’s going to contain all of our code (the main property in our package.json):

$ touch node-weather-cli.js

Create an account at Dark Sky and grab your secret key from your dev account page and set it as an environment variable in your system, I have named it DARK_SKY_API_KEY, but you can name whatever you want.

Now that we have all we need to develop or weather CLI. Let’s start coding!

Code

We require our dependencies and proceed to use the yargs library to parse the parameters given from the CLI execution:

const yargs = require('yargs');
const axios = require('axios');
const moment = require('moment');

const argv = yargs
  .options({
    a: {
      demand: true,
      alias: 'address',
      describe: 'Address to fetch weather for',
      string: true
    }
  })
  .help()
  .alias('help', 'h')
  .argv;

With this we can execute our script with:

$ node node-weather-cli.js -a berlin

And we’ll get the parameter in argv.address.
Knowing that, we can construct the URL for the MapQuest API which we’re going to be using in order to decode a given address into latitude and longitude coordinates:

const encodedAddress = encodeURIComponent(argv.address);
const url = `https://www.mapquestapi.com/geocoding/v1/address` +
  `?key=ntDn9JalUTAJXNehXiJMvnzqHz6DyeSq&location=${encodedAddress}`;

Next we’re going to use Axios, a promise based HTTP client for the browser and Node.js. We’ll fetch the MapQuest API, check if we get a valid latitude and longitude for the supplied address and make a request to the Dark Sky API with those coordinates:

axios.get(url)
  .then((response) => {
    if (!response.data.results) {
      throw new Error('Addres not found, please try again.');
    }

    const lat = response.data.results[0].locations[0].latLng.lat;
    const lng = response.data.results[0].locations[0].latLng.lng;
    if (lat === 39.390897 && lng === -99.066067) {
      throw new Error('Address not found, please try again.');
    }

    const api_key = process.env.DARK_SKY_API_KEY;
    const weatherUrl = `https://api.darksky.net/forecast/${api_key}/` +
      `${lat},${lng}?lang=en&units=si&exclude=hourly,flags`;

    return axios.get(weatherUrl);
  })
  .then((response) => {
    const todayWeatherData = response.data.daily.data[0];
    const todayWeatherForecast = getWeatherForecastFromData(todayWeatherData);
    printWeatherForecast(todayWeatherForecast);
  })
  .catch((error) => {
    if (error.code === 'ENOTFOUND') {
      console.log('Unable to connect to API servers.');
    } else {
      console.log(error.message);
    }
  });

The remaining functions are pretty straightforward, we have one to create an POJO (Plain old JavaScript object) from the data received from the Dark Sky API (we use Moment.js to deal with times):

const getWeatherForecastFromData = (data) => {
  return {
    summary: data.summary,
    maxTemp: data.temperatureMax,
    minTemp: data.temperatureMin,
    apparentMinTemp: data.apparentTemperatureMin,
    apparentMaxTemp: data.apparentTemperatureMax,
    humidity: data.humidity,
    cloudCover: data.cloudCover,
    precipitationProbability: data.precipProbability,
    precipitationIntensity: data.precipIntensity,
    uvIndex: data.uvIndex,
    uvDamageRisk: calculateUvDamageRisk(data.uvIndex),
    uvIndexTime: moment.unix(data.uvIndexTime).format('HH:mm'),
    windSpeed: data.windSpeed,
    sunriseTime: moment.unix(data.sunriseTime).format('HH:mm'),
    sunsetTime: moment.unix(data.sunsetTime).format('HH:mm')
  };
};

Another one to calculate the UV damage risk:

const calculateUvDamageRisk = (uvIndex) => {
  if (0 <= uvIndex && uvIndex < 3) return 'Low';
  if (3 <= uvIndex && uvIndex < 6) return 'Moderate';
  if (6 <= uvIndex && uvIndex < 8) return 'High';
  if (8 <= uvIndex && uvIndex < 11) return 'Very high';
  if (11 <= uvIndex) return 'Extreme';
};

And the last one to print a summary with our weather data:

const printWeatherForecast = (weather) => {
  console.log(`
Forecast for today ${moment().format('DD/MM/YYYY')}

${weather.summary}
    
Max temperature: ${weather.maxTemp}°C
Min temperature: ${weather.minTemp}°C
Min apparent temperature: ${weather.apparentMinTemp}°C
Max apparent temperature: ${weather.apparentMaxTemp}°C
Humidity: ${weather.humidity}
Cloud cover percentage: ${Math.trunc(weather.cloudCover * 100)}%
Precipitation probability: ${weather.precipitationProbability} with intensity of ${weather.precipitationIntensity}mm/h
UV index: ${weather.uvIndex}
Max UV index at: ${weather.uvIndexTime}
Damage risk due to unprotected sun exposure: ${weather.uvDamageRisk}
Wind speed: ${weather.windSpeed}m/s
Today the sun rises at: ${weather.sunriseTime}
Today the sun sets at: ${weather.sunsetTime}`);
};

With that we have our utility ready to be used:

$ node weather-cli-node.js -a berlin

Forecast for today 20/06/2019

Partly cloudy throughout the day.

Max temperature: 25.43°C
Min temperature: 18.68°C
Min apparent temperature: 18.68°C
Max apparent temperature: 25.43°C
Humidity: 0.62
Cloud cover percentage: 40%
Precipitation probability: 0.44 with intensity of 0.1027mm/h
UV index: 7
Max UV index at: 13:00
Damage risk due to unprotected sun exposure: High
Wind speed: 3.4m/s
Today the sun rises at: 04:44
Today the sun sets at: 21:34

That’s it, having it done in Python before made this one pretty straightforward to be honest.

You can check out the final code