Build a Command Line Weather App in Deno

Nilson Jacques
Share

If you’ve been following along with our introductory articles on Deno, you’re probably interested in having a go at writing your first program. In this article, we’re going to walk through installing the Deno runtime, and creating a command-line weather program that will take a city name as an argument and return the weather forecast for the next 24 hours.

To write code for Deno, I’d highly recommend Visual Studio Code with the official Deno plugin. To make things a little more interesting, we’re going to be writing the app in TypeScript.

Installing Deno

Firstly, let’s get Deno installed locally so we can begin writing our script. The process is straightforward, as there are installer scripts for all three major operating systems.

Windows

On windows, you can install Deno from PowerShell:

iwr https://deno.land/x/install/install.ps1 -useb | iex

Linux

From the Linux terminal, you can use the following command:

curl -fsSL https://deno.land/x/install/install.sh |  sh

macOS

On a Mac, Deno can be installed with Brew:

brew install deno

After installing

Once the install process is finished, you can check that Deno has been correctly installed by running the following command:

deno --version

You should now see something similar to this:

deno 1.2.0
v8 8.5.216
typescript 3.9.2

Let’s create a folder for our new project (inside your home folder, or wherever you like to keep your coding projects) and add an index.ts file:

mkdir weather-app
cd weather-app
code index.ts

Note: as I mentioned above, I’m using VS Code for this tutorial. If you’re using a different editor, replace the last line above.

Getting User Input

Our program is going to retrieve the weather forecast for a given city, so we’ll need to accept the city name as an argument when the program is run. Arguments supplied to a Deno script are available as Deno.args. Let’s log this variable out to the console to see how it works:

console.log(Deno.args);

Now run the script, with the following command:

deno run index.ts --city London

You should see the following output:

[ "--city", "London" ]

Although we could parse this argument array ourselves, Deno’s standard library includes a module called flags that will take care of this for us. To use it, all we have to do is add an import statement to the top of our file:

import { parse } from  "https://deno.land/std@0.61.0/flags/mod.ts";

Note: the examples in the docs for standard library modules will give you an unversioned URL (such as https://deno.land/std/flags/mod.ts), which will always point to the latest version of the code. It’s good practice to specify a version in your imports, to ensure your program isn’t broken by future updates.*

Let’s use the imported function to parse the arguments array into something more useful:

const args = parse(Deno.args);

We’ll also change the script to log out our new args variable, to see what that looks like. So now your code should look like this:

import { parse } from  "https://deno.land/std@0.61.0/flags/mod.ts";

const args = parse(Deno.args);

console.log(args);

Now, if you run the script with the same argument as before, you should see the following output:

Download https://deno.land/std@0.61.0/flags/mod.ts
Download https://deno.land/std@0.61.0/_util/assert.ts
Check file:///home/njacques/code/weather-app/index.ts
{ _: [], city: "London" }

Whenever Deno runs a script, it checks for new import statements. Any remotely hosted imports are downloaded, compiled, and cached for future use. The parse function has provided us with an object, which has a city property containing our input.

Note: if you need to re-download the imports for a script for any reason, you can run deno cache --reload index.ts.

We should also add a check for the city argument, and quit the program with an error message if it’s not supplied:

if (args.city === undefined) {
    console.error("No city supplied");
    Deno.exit();
}

Talking to the Weather API

We’re going to be getting our forecast data from OpenWeatherMap. You’ll need to register for a free account, in order to obtain an API key. We’ll be using their 5 day forecast API, passing it a city name as a parameter.

Let’s add some code to fetch the forecast and log it out to the console, to see what we get:

import { parse } from  "https://deno.land/std@0.61.0/flags/mod.ts";

const args = parse(Deno.args);

if (args.city === undefined) {
    console.error("No city supplied");
    Deno.exit();
}

const apiKey = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';

const res = await fetch(`https://api.openweathermap.org/data/2.5/forecast?q=${args.city}&units=metric&appid=${apiKey}`);
const data = await res.json();

console.log(data);

Deno tries to support a lot of browser APIs where possible, so here we can use fetch without having to import any external dependencies. We’re also making use of the support for top-level await: normally we’d have to wrap any code that uses await in an async function, but TypeScript doesn’t make us do this, which makes the code a little nicer.

If you try running this script now, you’ll encounter an error message:

Check file:///home/njacques/code/weather-app/index.ts
error: Uncaught PermissionDenied: network access to "https://api.openweathermap.org/data/2.5/forecast?q=London&units=metric&appid=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", run again with the --allow-net flag
    at unwrapResponse ($deno$/ops/dispatch_json.ts:42:11)
    at Object.sendAsync ($deno$/ops/dispatch_json.ts:93:10)
    at async fetch ($deno$/web/fetch.ts:266:27)
    at async index.ts:12:13

By default, all Deno scripts are run in a secure sandbox: they don’t have access to the network, the filesystem, or things like environment variables. Scripts need to be explicitly granted permission for the system resources they need to access. In this case, the error message helpfully lets us know which permission we need and how to enable it.

Let’s call the script again, with the correct flag:

deno run --allow-net index.ts --city London

This time, we should get back a JSON response from the API:

{
  cod: "200",
  message: 0,
  cnt: 40,
  list: [
    {
      dt: 1595527200,
      main: {
        temp: 22.6,
        feels_like: 18.7,
        temp_min: 21.04,
        temp_max: 22.6,
        pressure: 1013,
        sea_level: 1013,
        grnd_level: 1011,
        humidity: 39,
        temp_kf: 1.56
      },
      weather: [ [Object] ],
      clouds: { all: 88 },
      wind: { speed: 4.88, deg: 254 },
      visibility: 10000,
      pop: 0,
      sys: { pod: "d" },
      dt_txt: "2020-07-23 18:00:00"
    },
    ...
  ],
  city: {
    id: 2643743,
    name: "London",
    coord: { lat: 51.5085, lon: -0.1257 },
    country: "GB",
    population: 1000000,
    timezone: 3600,
    sunrise: 1595477494,
    sunset: 1595534525
  }
}

You can check out the full details of what gets returned in the response, but what we’re interested in mainly is the array of forecast data in list. Each object in the array contains a timestamp (dt), a main object with details of the atmospheric conditions (temperature, humidity, pressure etc.), and a weather array containing an object with a description of the predicted weather.

We’re going to iterate over the main array to get the forecast time, temperature, and weather conditions. Let’s start by limiting the number of records to cover a 24-hour period only. The forecast data available to us on the free plan is only available in three-hour intervals, so we’ll need to get eight records:

const  forecast = data.list.slice(0, 8)

We’ll map over each of the forecast items, and return an array of the data we’re interested in:

const forecast = data.list.slice(0, 8).map(item => [
    item.dt,
    item.main.temp,
    item.weather[0].description,
]);

If we try to run the script now, we’ll get a compile error (if you’re using an IDE like VS Code, you’ll also get this error displayed as you type the code): Parameter ‘item’ implicitly has an ‘any’ type.

TypeScript requires us to tell it about the type of variable that item is, in order to know if we’re doing anything with it that could cause an error at runtime. Let’s add an interface, to describe the structure of item:

interface forecastItem {
    dt: string;
    main: { temp: number; };
    weather: { description: string; }[];
}

Note that we’re not describing all the properties of the object here, only the ones we’re actually going to access. In our situation, we know which properties we want.

Let’s add our new type to our map callback:

const forecast = data.list.slice(0, 8).map((item: forecastItem) => [
    item.dt,
    item.main.temp,
    item.weather[0].description,
]);

If you’re using an IDE with TypeScript support, it should be able to autocomplete the properties of item as you type, thanks to the interface type we’ve supplied.

  • Create a service class
  • Create an interface for the output

Formatting the Output

Now that we have the set of data we want, let’s look at formatting it nicely to display to the user.

First off, let’s transform the timestamp value into a human-readable date. If we take a look at Deno’s third-party module list and search for “date”, we can see date-fns in the list. We can use the link from here to import the functions we’re going to use into our Deno app:

import { fromUnixTime, format } from  "https://deno.land/x/date_fns@v2.15.0/index.js";

We can now pass the timestamp through the fromUnixTime function, to get a Date object, and then pass this object into format in order to get a date string we want:

format(fromUnixTime(item.dt), "do LLL, k:mm", {})

The formatting string do LLL, k:mm will give us a date in the following format: “24th Jul, 13:00”.

Note: we’re passing an empty object as the third argument to format purely to silence an IDE warning about the expected number of arguments. The code will still run fine without it.

While we’re at it, let’s round the temperature value to a single decimal place, and add a units indicator:

`${item.main.temp.toFixed(1)}C`

Now that we have our forecast data formatted and ready to display, let’s present it to the user in a neat little table, using the ascii_table module:

import  AsciiTable  from  'https://deno.land/x/ascii_table/mod.ts';

...

const table = AsciiTable.fromJSON({
  title: `${data.city.name} Forecast`,
  heading: [ 'Time', 'Temp', 'Weather'],
  rows: forecast
})

console.log(table.toString())

Save and run the script, and now we should have nicely formatted and presented forecast for our chosen city, for the next 24 hours:

.--------------------------------------------.
|              London Forecast               |
|--------------------------------------------|
|      Time       | Temp  |     Weather      |
|-----------------|-------|------------------|
| 23rd Jul, 19:00 | 17.8C | light rain       |
| 23rd Jul, 22:00 | 16.8C | light rain       |
| 24th Jul, 1:00  | 16.0C | broken clouds    |
| 24th Jul, 4:00  | 15.6C | light rain       |
| 24th Jul, 7:00  | 16.0C | broken clouds    |
| 24th Jul, 10:00 | 18.3C | scattered clouds |
| 24th Jul, 13:00 | 20.2C | light rain       |
| 24th Jul, 16:00 | 20.2C | light rain       |
'--------------------------------------------'

Complete Code Listing

It’s quite a compact script, but here’s the complete code listing:

import { parse } from "https://deno.land/std@0.61.0/flags/mod.ts";
import {
  fromUnixTime,
  format,
} from "https://deno.land/x/date_fns@v2.15.0/index.js";
import AsciiTable from "https://deno.land/x/ascii_table/mod.ts";

const args = parse(Deno.args);

if (args.city === undefined) {
  console.error("No city supplied");
  Deno.exit();
}

const apiKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";

const res = await fetch(
  `https://api.openweathermap.org/data/2.5/forecast?q=${args.city}&units=metric&appid=${apiKey}`,
);
const data = await res.json();

interface forecastItem {
  dt: string;
  main: { temp: number };
  weather: { description: string }[];
}

const forecast = data.list.slice(0, 8).map((item: forecastItem) => [
  format(fromUnixTime(item.dt), "do LLL, k:mm", {}),
  `${item.main.temp.toFixed(1)}C`,
  item.weather[0].description,
]);

const table = AsciiTable.fromJSON({
  title: `${data.city.name} Forecast`,
  heading: ["Time", "Temp", "Weather"],
  rows: forecast,
});

console.log(table.toString());

Summary

You now have your own working Deno command-line program that will give you the weather forecast for the next 24 hours. By following along with this tutorial, you should now be familiar with how to start a new program, import dependencies from the standard library and third parties, and grant script permissions.

So, having got a taste for writing programs for Deno, where should you go next? I’d definitely recommend having a read through the manual to learn more about the various command-line options and built-in APIs, but also keep your eye on SitePoint for more Deno content!

Deno Foundations

Get up to speed with Deno. Our Deno Foundations collection helps you take your first steps into the Deno world and beyond, and we’re adding to it constantly. We’ll bring you the tutorials you need to become a pro. You can always refer to our index as it’s updated at the end of our Introduction to Deno:

Deno Foundations