🦕 Running your first Deno script
Tags: javascript, typescript, tutorial, deno
Published: 2021-07-08 16:26:45

In this post, we are going from zero to running a small but nontrivial script that fetches comments from Hacker News in real time using [Deno](https://deno.land/), a new runtime for JavaScript and [TypeScript](https://www.typescriptlang.org/) created by [Ryan Dahl](https://github.com/ry), the original creator of [Node.js](https://nodejs.org/).

## What is Deno?

Oversimplifying, we can say that Deno is an alternative to Node.js.

More precisely, we can read the following description on [Deno's website](https://deno.land/):

> Deno is a simple, modern and secure runtime for JavaScript and TypeScript that uses V8 and is built in Rust.

Let's delve deeper into three key characteristics that distinguish Deno from Node.js:

- **Simple**: as we will soon see, Deno is simple to install, being shipped as a single executable file; Deno is also simple to use for small and larger scripts thanks to its powerful CLI, modern features and standard library; finally, learning Deno is also simple thanks to its short and execellent [manual](https://deno.land/manual)

- **Modern**: Deno is built with modern technologies and follows a modern philosophy; it natively supports [TypeScript](https://www.typescriptlang.org/) and [ES modules](https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/); it doesn't require a centralized package management solution; it also provides useful developer tools out of the box such as an opinionated source code formatter and a testing framework

- **Secure**: by default, Deno prevents access to critical resources such as files, network connections and environment variables; using these resources requires an explicit permission grant by the user

Now that we have got to know Deno better, let's start using it.

## What are we building?

We are going to write a script that reads the stream of comments posted to [Hacker News](https://news.ycombinator.com/) in real time and prints them in the console.

We will use TypeScript to write the script and Deno to run it; prior experience in TypeScript or Deno is not required since we will go step by step.

I assume that you are using a Linux distribution where a text editor and a terminal are available, however you should be able to follow the same instructions on Windows or Mac with minimal differences.

When following along, you can refer to the [source code for the script](https://gist.github.com/velut/6d0335c3ede885e11ce7c5768107d103), integrating it with the [Deno manual](https://deno.land/manual) and the [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/intro.html) if necessary.

## Setup

Let's start by installing Deno on our machine and verifying that it works.

According to [Deno's installation instructions](https://deno.land/manual@v1.11.5/getting_started/installation), we need to run the following command in our terminal:

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

The command above requires having both `curl` and `unzip` installed on our system.

After the command finishes running, we should see the following output:

```bash
Archive:  /home/<your_username>/.deno/bin/deno.zip
  inflating: /home/<your_username>/.deno/bin/deno
Deno was installed successfully to /home/<your_username>/.deno/bin/deno
Manually add the directory to your $HOME/.bash_profile (or similar)
  export DENO_INSTALL="/home/<your_username>/.deno"
  export PATH="$DENO_INSTALL/bin:$PATH"
```

As the message suggests, we need to edit (or create) the profile file used by our shell, for example `$HOME/.profile` or `$HOME/.bash_profile`, and add these two lines at the bottom:

```bash
export DENO_INSTALL="/home/<your_username>/.deno"
export PATH="$DENO_INSTALL/bin:$PATH"
```

Make sure to paste the lines actually displayed in **your terminal** by Deno.

After saving the profile file and restarting the terminal, we should be able to write `deno` and press enter to see the following output:

```bash
$ deno
Deno 1.11.5
exit using ctrl+d or close()
>
```

As suggested, we can exit from Deno by pressing `Ctrl` and `D` together.

Additionally, running `deno run https://deno.land/std/examples/welcome.ts` in the terminal should display the following message:

```bash
$ deno run https://deno.land/std/examples/welcome.ts
Welcome to Deno!
```

If setting the `PATH` environment variable is not possible, we can still call `deno` by its relative path, for example:

```bash
$ .deno/bin/deno
Deno 1.11.5
exit using ctrl+d or close()
>
```

Now that Deno is correctly installed, we can start writing our script.

## Step 1: Create the `main.ts` file

In an empty directory, let's create a file named `main.ts` (the name `main` has no importance) that looks like this:

```typescript
// main.ts

console.log("Hello from Deno!");
```

Let's open a terminal in the same directory and run `deno run main.ts` to see the following output:

```bash
$ deno run main.ts
Check file:///home/<your_username>/<some_directory>/main.ts
Hello from Deno!
```

As we can see, we don't need a `package.json` file or any external dependencies to run this TypeScript file.

We can also run `deno fmt main.ts` or use the [Deno extension for VSCode](https://marketplace.visualstudio.com/items?itemName=denoland.vscode-deno) to automatically format the source code in this file.

## Step 2: Define the base URL for the Hacker News API

Since we will interact with the [Hacker News API](https://github.com/HackerNews/API), let's clear the `main.ts` file and define the `baseURL` variable as follows:

```typescript
// main.ts

/** Base URL for all calls to the Hacker News API */
const baseURL = "https://hacker-news.firebaseio.com/v0";
```

This URL is the target for the HTTP requests we will make in later steps.

## Step 3: Define the `Item` interface

The Hacker News API represents user-generated content, including comments, as [items](https://github.com/HackerNews/API#items) with various properties. We can identify items using the properties `id`, a unique incrementing integer number, and `type`, an enumeration of different item types.

Following the [official API specification](https://github.com/HackerNews/API#items), let's model an item using a [TypeScript interface](https://www.typescriptlang.org/docs/handbook/2/objects.html):

```typescript
// main.ts

const baseURL = "https://hacker-news.firebaseio.com/v0";

/** Item represents an item fetched from the HN API */
interface Item {
  /** Unique item ID; the only required property */
  readonly id: number;
  /** Item type (`job`, `story`, `comment`, `poll`, or `pollopt`) */
  readonly type?: string;
  /** Username of the user who submitted the item */
  readonly by?: string;
  /** Title text for a story, poll or job */
  readonly title?: string;
  /** URL for a story or job */
  readonly url?: string;
  /** Text for a story, comment, poll, poll option or job */
  readonly text?: string;
  /** Unix timestamp for when the item was created */
  readonly time?: number;
  /** Score for a story, job or poll; votes for a poll option */
  readonly score?: number;
  /** Number of total comments for a story or poll */
  readonly descendants?: number;
  /** Set to `true` if the item is deleted */
  readonly deleted?: boolean;
  /** Set to `true` if the item is dead */
  readonly dead?: boolean;
  /** ID of the parent item of a comment (a story or another comment) */
  readonly parent?: number;
  /** List of IDs of the item's comments, in display order */
  readonly kids?: number[];
  /** ID of the poll associated to a poll option */
  readonly poll?: number;
  /** List of IDs of related poll options, in display order */
  readonly parts?: number[];
}
```

Note that only the `id` property is required, all other properties are [marked as optional](https://www.typescriptlang.org/docs/handbook/2/objects.html#optional-properties) with `?`. All properties are also [marked as `readonly`](https://www.typescriptlang.org/docs/handbook/2/objects.html#readonly-properties) so that they cannot be reassigned.

We won't be using all the properties present in `Item`, but they have been defined and documented for completeness.

## Step 4: Get the ID of the most recent item

Since item IDs are represented as an incrementing integer, the most recent item is the one with the largest ID. To get this value, we can use the `/maxitem.json` endpoint, that is [https://hacker-news.firebaseio.com/v0/maxitem.json](https://hacker-news.firebaseio.com/v0/maxitem.json).

Let's implement an [asynchronous function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) named `getMaxItemID()` that fetches this data:

```typescript
// main.ts

const baseURL = "https://hacker-news.firebaseio.com/v0";

interface Item {
  // Omitted for brevity
}

/** getMaxItemID returns the ID of the most recent item published on HN */
async function getMaxItemID(): Promise<number> {
  const endpoint = `${baseURL}/maxitem.json`;
  const res = await fetch(endpoint);
  const id = await res.json();
  return id;
}
```

We first define the endpoint, then use the `fetch` web API [implemented by Deno](https://deno.land/manual@v1.11.5/examples/fetch_data) to retrieve the JSON data representing the largest item ID.

As a quick test, let's also add this line below our function:

```typescript
// main.ts

// ...

async function getMaxItemID(): Promise<number> {
  const endpoint = `${baseURL}/maxitem.json`;
  const res = await fetch(endpoint);
  const id = await res.json();
  return id;
}

// Remove this line after this step
console.log(await getMaxItemID());
```

Now let's run `deno run main.ts`:

```bash
$ deno run main.ts
error: Uncaught (in promise) PermissionDenied: Requires net access to "hacker-news.firebaseio.com", run again with the --allow-net flag
  const res = await fetch(endpoint);
                    ^
    at deno:core/core.js:86:46
    at unwrapOpResult (deno:core/core.js:106:13)
    at Object.opSync (deno:core/core.js:120:12)
    at opFetch (deno:extensions/fetch/26_fetch.js:43:17)
    at mainFetch (deno:extensions/fetch/26_fetch.js:170:61)
    at deno:extensions/fetch/26_fetch.js:395:7
    at new Promise (<anonymous>)
    at fetch (deno:extensions/fetch/26_fetch.js:357:15)
    at getMaxItemID (file:///home/<your_username>/<some_directory>/main.ts:43:21)
    at file:///home/<your_username>/<some_directory>/main.ts:48:1
```

Since Deno is secure by default, it prevented our script from accessing the network connection to fetch data from `hacker-news.firebaseio.com`. As explained in the error message, we need to use the [`--allow-net` flag](https://deno.land/manual@v1.11.5/getting_started/permissions#network-access) to explicitly grant this permission when running the script:

```bash
$ deno run --allow-net main.ts
27121843
```

Now that the script can reach the API, we can see the ID of the most recent item being printed in the console.

Before going to the next step, let's remove the line `console.log(await getMaxItemID());` that we just added.

## Step 5: Get the most recent item by its ID

Now that we have the ID of the most recent item, we can get the item itself by using the `/item/<id>.json` endpoint, for example [https://hacker-news.firebaseio.com/v0/item/27121843.json](https://hacker-news.firebaseio.com/v0/item/27121843.json).

Let's implement another asynchronous function named `getItemByID()` that fetches an item given its ID:

```typescript
// main.ts

// ...

/** getItemByID fetches an item from the HN API given its ID */
async function getItemByID(id: number): Promise<Item | undefined> {
  const endpoint = `${baseURL}/item/${id}.json`;
  const res = await fetch(endpoint);
  const item = await res.json();
  return item ?? undefined;
}
```

If the ID is valid, `getItemByID()` will return an object described by the `Item` interface; otherwise, it will return `undefined`. The [nullish coalescing operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator) (`??`) is supported out of the box by Deno.

We can now fetch and print the latest item by adding the line `console.log(await getItemByID(await getMaxItemID()));` as shown:

```typescript
// main.ts

// ...

async function getItemByID(id: number): Promise<Item | undefined> {
  const endpoint = `${baseURL}/item/${id}.json`;
  const res = await fetch(endpoint);
  const item = await res.json();
  return item ?? undefined;
}

// Remove this line after this step
console.log(await getItemByID(await getMaxItemID()));
```

Running again our script with `deno run --allow-net main.ts` should display an item similar to this:

```bash
$ deno run --allow-net main.ts
Check file:///home/<your_username>/<some_directory>/main.ts
{
  by: "velut",
  id: 27121843,
  parent: 27121265,
  text: "Thank you!<p>Yes, I use prism-react-renderer for syntax highlighting.<p>I did not know about Shiki, ...",
  time: 1620758446,
  type: "comment"
}
```

Note that the item displayed in your terminal will be different and won't necessarily be a comment. Sometimes it may also be `undefined` if the HN API has not properly updated yet internally.

## Step 6: Stream comments from Hacker News

Now that we have the necessary building blocks, let's implement the `streamComments()` function.

First, let's get the ID of the most recent item:

```typescript
// main.ts

// ...

/** streamComments continuously fetches and displays the most recent comments published on HN */
async function streamComments() {
  // Get the ID of the most recent item
  let id = await getMaxItemID();
}
```

Then, get the item, increment the ID for the next item and loop forever:

```typescript
// main.ts

// ...

async function streamComments() {
  let id = await getMaxItemID();

  // Keep running forever
  while (true) {
    // Get the item
    const item = await getItemByID(id);

    // Increment the ID for the next item
    id += 1;
  }
}
```

However, we need to wait a little if an item still does not exist. To do so, let's [import a remote module](https://deno.land/manual@v1.11.5/examples/import_export#remote-import) containing a `sleep` function:

```typescript
// main.ts

// Import the `sleep` function from a remote module
import { sleep } from "https://deno.land/x/sleep/mod.ts";

// ...

async function streamComments() {
  let id = await getMaxItemID();

  while (true) {
    const item = await getItemByID(id);

    // If the item does not exist, wait 5 seconds and try again
    if (!item) {
      await sleep(5);
      continue;
    }

    id += 1;
  }
}
```

To import modules in our Deno scripts, we use file paths for local modules (for example, `import ... from "./some/local/module.ts"`) and URLs for remote modules (for example, `import ... from "https://example.com/some/remote/module.ts`).

We don't need to install packages from a centralized remote repository like [npm](https://www.npmjs.com/), Deno will instead download and cache the remote modules and their dependencies the first time they are used.

Let's now complete the `streamComments()` function by printing only comments:

```typescript
// main.ts

import { sleep } from "https://deno.land/x/sleep/mod.ts";

// ...

async function streamComments() {
  let id = await getMaxItemID();

  while (true) {
    const item = await getItemByID(id);
    if (!item) {
      await sleep(5);
      continue;
    }

    // Print only items that are visible comments
    const { type, deleted, dead } = item;
    const removed = deleted || dead || false;
    if (type === "comment" && !removed) {
      // Get the comment's author, if any
      const author = item.by ?? "unknown author";

      // Get the comment's URL on HN website
      const hnURL = `https://news.ycombinator.com/item?id=${id}`;

      // Print the comment
      console.log(`\nRead new comment by ${author} on ${hnURL}`);
      console.log(item);
    }

    id += 1;
  }
}
```

Let's finish our script by calling `await streamComments()` at the bottom:

```typescript
// main.ts

// ...

async function streamComments() {
  // ...
}

await streamComments();
```

You can compare your script with [the final version of the script](https://gist.github.com/velut/6d0335c3ede885e11ce7c5768107d103).

## Step 7: Running the script

Now that our script is complete, we can run it with the same `deno run --allow-net main.ts` command used before:

```bash
$ deno run --allow-net main.ts
Download https://deno.land/x/sleep/mod.ts
Warning Implicitly using latest version (v1.2.0) for https://deno.land/x/sleep/mod.ts
Download https://deno.land/x/sleep@v1.2.0/mod.ts
Download https://deno.land/x/sleep@v1.2.0/sleep.ts
Download https://deno.land/std/log/mod.ts
<...many other downloads...>
Check file:///home/<your_username>/<some_directory>/main.ts

Read new comment by tonyedgecombe on https://news.ycombinator.com/item?id=27762774
{
  by: "tonyedgecombe",
  id: 27762774,
  parent: 27761864,
  text: "&gt;The printer stack which has barely received an update since Windows XP is a perfect example.<p>T...",
  time: 1625674491,
  type: "comment"
}

Read new comment by jph on https://news.ycombinator.com/item?id=27762775
{
  by: "jph",
  id: 27762775,
  parent: 27762699,
  text: "The catchphrase is &quot;neurons that fire together wire together&quot;.",
  time: 1625674509,
  type: "comment"
}

Read new comment by exo-pla-net on https://news.ycombinator.com/item?id=27762777
{
  by: "exo-pla-net",
  id: 27762777,
  parent: 27762326,
  text: "The plant kingdom is almost entirely mono-food-sourced. They almost all rely on the sun, which, pend...",
  time: 1625674519,
  type: "comment"
}
```

As we can see, Deno first downloads the required remote modules and then starts running our script. After a few seconds, we should be able to read the stream of comments being published to Hacker News in real time.

## Conclusion

In this post, we discovered Deno, a simple, modern and secure alternative to Node.js, and explored many of its built-in features such as TypeScript support, web APIs, ES modules imports and modern Javascript syntax. These features allowed us to quickly implement and run a small script that fetches data in real time from a third party API.

You can learn more about Deno from its [website](https://deno.land/) and [manual](https://deno.land/manual). When developing more complex scripts, you may also want to use Deno's [standard library](https://deno.land/std@0.100.0) or search for [third party modules](https://deno.land/x).

If you liked this article and want to know when I post more, you can [follow me on Twitter](https://twitter.com/EdoardoScibona).

## Credits

- Cover photo by <a href="https://unsplash.com/@katie_s?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Katie Smith</a> on <a href="https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>