Key Takeaways
- This tutorial demonstrates how to install Deno, a secure runtime for JavaScript and TypeScript, and build a command-line weather forecast application using it.
- The application uses the OpenWeatherMap API to fetch forecast data, requiring the user to register for a free account to obtain an API key. The forecast data is then formatted and presented to the user in a table.
- Deno scripts are run in a secure sandbox by default, with no access to the network, filesystem, or environment variables. Scripts must be explicitly granted permission for the system resources they need to access.
- The application uses third-party modules, including ‘date-fns’ for date formatting and ‘ascii_table’ for table creation. These are imported using URLs, a departure from Node.js’s module system.
- The tutorial ends with a complete code listing for the application, allowing readers to follow along and create their own Deno command-line weather forecast application.
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 asDeno.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 FoundationsFrequently 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 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.