Build a Command Line Weather App in Deno

Share this article

Build a Terminal Weather App in Deno
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

Frequently Asked Questions (FAQs) about Building a Command Line Weather App with Deno

How can I install Deno on my system?

To install Deno on your system, you can use the installation scripts available on the Deno website. For macOS, Linux, or Shell, you can use curl or PowerShell for Windows. The installation process is straightforward and doesn’t require any complex configurations. Once installed, you can verify the installation by typing ‘deno –version’ in your terminal. This will display the installed version of Deno.

What are the key differences between Node.js and Deno?

Deno is a secure runtime for JavaScript and TypeScript, created by the original developer of Node.js. Unlike Node.js, Deno supports TypeScript out of the box, has a built-in package manager, and emphasizes security by running scripts in a secure sandbox environment. This means that by default, Deno scripts cannot access the file system, network, or environment variables unless explicitly allowed.

How can I fetch data from an API using Deno?

Fetching data from an API in Deno is similar to how you would do it in a browser. You can use the fetch API, which returns a promise that resolves to the Response object. You can then use the .json() method to parse the response body as JSON.

How can I handle errors in Deno?

Error handling in Deno is done using try-catch blocks, similar to other JavaScript environments. If an error occurs in the try block, the catch block is executed. This allows you to handle errors gracefully and provide useful feedback to the user.

How can I test my Deno applications?

Deno comes with a built-in test runner that you can use to test your applications. You can write tests using the Deno.test function and run them using the ‘deno test’ command. Deno also supports test-driven development (TDD) out of the box.

How can I use third-party modules in Deno?

Deno uses URLs for importing modules, which is a departure from Node.js’s module system. You can import a module by providing its URL in the import statement. Deno also provides a centralized repository for modules, similar to npm, where you can find and use third-party modules.

How can I build a CLI with Deno?

Building a CLI with Deno involves creating a script that can be run from the command line. You can use the Deno runtime API to interact with the system, such as reading from and writing to the file system, making network requests, and more. You can also use third-party modules to help with parsing command-line arguments and displaying help messages.

How can I deploy my Deno applications?

Deploying Deno applications can be done using Docker or cloud platforms that support Deno, such as Deno Deploy or Heroku. You can also use serverless platforms like Vercel or AWS Lambda with a custom runtime.

How can I debug Deno applications?

Deno supports debugging through the Chrome DevTools. You can start a debugging session by running your script with the ‘deno run –inspect’ command, which will print a WebSocket URL. You can then open Chrome DevTools and use this URL to connect to the running script.

How can I contribute to the Deno project?

The Deno project is open source and welcomes contributions from the community. You can contribute by reporting bugs, suggesting features, improving documentation, or submitting pull requests. Before contributing, it’s recommended to read the contributing guide on the Deno GitHub repository.

Nilson JacquesNilson Jacques
View Author

Nilson is a full-stack web developer who has been working with computers and the web for over a decade. A former hardware technician, and network administrator. Nilson is now currently co-founder and developer of a company developing web applications for the construction industry. You can also find Nilson on the SitePoint Forums as a mentor.

Command lineDenoDeno runtimeTypeScript
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week