Designing a URL Shortener in Deno
Tags: webdev, javascript, typescript, deno
Published: 2021-01-23 04:52:20



To understand the basics of Deno and learn how to start a Deno project from scratch, check out the YouTube video above. In this article, we’re going to jump into the deep end with Deno and build a complete application. If you need help with anything JS, feel free to reach out through [Superpeer](https://superpeer.com/akash) (a video-chat platform) or [Twitter](https://twitter.com/thewritingdev).

What we will achieve:

1. Mapping URL shortcodes to endpoints using a live-editable JSON file.
2. Have expiry dates attached to each URL so that shortcodes are valid only for a limited period of time.

## 0. Prerequisites

1. Having [Deno](https://deno.land/#installation) installed.
2. Knowing how to use `deno run —allow-net —allow-read /path/to/file.ts` to run your code.
3. Following this tutorial to get an initial repository ready.

### So, let’s get started 🔥

## 1. Building the Router

If we look at the Oak module used from the video: [https://deno.land/x/oak](https://deno.land/x/oak), the "[Basic Usage](https://deno.land/x/oak#basic-usage)" section pretty much covers all the use cases of a router. So, what we will do is expand on the existing code. 

To test this code, you can create a file called `index.ts` in a folder, and copy the "Basic Usage" code into it.

To understand how to run TypeScript or JavaScript files in Deno, you first need to understand how Deno runs files. You run a file by running the command `deno run file_name.ts` or `file_name.js` depending on whether it's TypeScript or JavaScript.

Run it using the command `deno run —allow-net index.ts`. You add the `allow-net` so your script has network access.

The "Basic Usage” router looks like this:

```jsx
router
  .get("/", (context) => {
    context.response.body = "Hello world!";
  })
  .get("/book", (context) => {
    context.response.body = Array.from(books.values());
  })
  .get("/book/:id", (context) => {
    if (context.params && context.params.id && books.has(context.params.id)) {
      context.response.body = books.get(context.params.id);
    }
  });
```

Here, we can keep the “/“ endpoint unchanged to test whether the router is running without errors and get a default response. We don’t need the “/book” URL, so it can be removed. We can keep the "/" endpoint, as it is a good example of how normal endpoints will look in Oak.

## 2. Building the shortener

To build a URL shortener, let's consider the logic we'll use for mapping shortened URLs with their final endpoints. Let's create a file, `urls.json`, which will have the format

```jsx
{
  "shortcode": {
    "dest": "destination_url_string",
    "expiryDate": "YYYY-MM-DD"
  }
}
```

We will have a key for each url shortcode, defined here as "shortcode". For each shortcode, we will have a destination URL "dest" and a date when the URL is no longer valid "expiryDate". You can check the JSON file here: [https://github.com/akash-joshi/deno-url-shortener/blob/master/urls.json](https://github.com/akash-joshi/deno-url-shortener/blob/master/urls.json).

To read this JSON file in your code, add the following to the top of `index.ts`

```jsx
import { Application, Router } from "https://deno.land/x/oak/mod.ts";

const urls = JSON.parse(Deno.readTextFileSync("./urls.json"));

console.log(urls);
```

Now, to run your `index.ts`, you will need another flag `—allow-read`. Your final command becomes `deno run —allow-net —allow-read index.ts`. After running this command, you'll see the JSON file being printed in your terminal window. This means that your program is able to read the JSON file correctly.

From the Basic Usage example, “/book/:id” is exactly what we need. Instead of "/book/:id", we can use "/shrt/:urlid", where we will get the individual URLs based on the URL ID. Replace the existing code present inside the "/book/:id" route with this one:

```jsx
.get("/shrt/:urlid", (context) => {
    if (context.params && context.params.urlid && urls[context.params.urlid]) {
      context.response.redirect(urls[context.params.urlid].dest);
    } else {
      context.response.body = "404";
    }
  });
```

The if condition in the route does the following:

1. Checks if parameters are attached to the route
2. Checks if the parameter `urlid` is in the parameter list.
3. Checks whether the `urlid` matches with any url in our json.

If it matches with all these, the user is redirected to the correct URL. If it doesn't, a 404 response on the body is returned.

To test this, copy this route into `index.ts`, to make it look like

```jsx
router
  .get("/", (context) => {
    context.response.body = "Hello world!";
  })
	.get("/shrt/:urlid", (context) => {
	    if (context.params && context.params.urlid && urls[context.params.urlid]) {
	      context.response.redirect(urls[context.params.urlid].dest);
	    } else {
	      context.response.body = "404";
	    }
	  });
```

And run the file using `deno run —allow-net —allow-read index.ts`.

Now, if you go to `http://localhost:8000/shrt/g`, you'll be redirected to Google's homepage. On the other hand, using a random shortcode after /shrt/ brings you to the 404 page. However, you'll see that the shortener doesn't react live to changes in the json file. This is because `urls.json` is only read once.

## 3. Add Live-Reloading

To make the `urls` object react live to changes in the JSON file, we simply move the read statement inside our route.

```jsx
.get("/shrt/:urlid", (context) => {
  const urls = JSON.parse(Deno.readTextFileSync("./urls.json"));

  if (context.params && context.params.urlid && urls[context.params.urlid]) {
    context.response.redirect(urls[context.params.urlid].dest);
  } else {
    context.response.body = "404";
  }
});
```

Now even if we add or remove routes on the fly, our program will react to it.

## 4. Adding Expiry to the URLs

To make our URLs expire according to dates, we will be using the popular momentjs library, which  luckily, has been ported to Deno: [https://deno.land/x/moment](https://deno.land/x/moment). To understand how moment works, check out its documentation in the above link. 

To use it in our program, import it directly through its URL like this:

```jsx
import { Application, Router } from "https://deno.land/x/oak/mod.ts";
import { moment } from "https://deno.land/x/moment/moment.ts";

const router = new Router();
```

To check the date for when the URL will expire, we check the `expiryDate` key on our `urls` object. This will make the program look like:

```jsx
if (context.params && context.params.urlid && urls[context.params.urlid]) {
  if (
    urls[context.params.urlid].expiryDate > moment().format("YYYY-MM-DD")
  ) {
    context.response.redirect(urls[context.params.urlid].dest);
  } else {
    context.response.body = "Link Expired";
  }
} else {
  context.response.body = "404";
}
```

In `moment().format("YYYY-MM-DD")`, we get the current datetime using `moment()` and convert it to the "YYYY-MM-DD" format using `.format("YYYY-MM-DD")`. By comparing it against our `expiryDate` key, we can check whether the URL has expired or not.

That's it ! You have built a fully functional URL shortener in Deno. You can find the final code in the GitHub repo at [https://github.com/akash-joshi/deno-url-shortener](https://github.com/akash-joshi/deno-url-shortener). 

If you need help with anything JS, feel free to reach out through [Superpeer](https://superpeer.com/akash) (a video-chat platform) or [Twitter](https://twitter.com/thewritingdev).

### My Thoughts on Deno

While it's refreshing to see a server-side language which takes security into consideration and supports TypeScript out-of-the-box, Deno still has a long way to go before being ready for use in production systems. For example, the TypeScript compilation is still very slow, with compilation times ~20 seconds even for simple programs like the one we just developed.

On the Deno side, it still is pretty bad with error-reporting. For example, while embedding the code to read `urls.json` in the function itself, Deno isn't able to report that the `-allow-read` flag hasn't been set. Instead, it just throws a 500 without a proper error printed on the terminal.

### What Next ?

You can improve your Deno or Typescript skills by building more complex applications like a [Chatting Application](https://github.com/akash-joshi/Open-Chat) or a [Wikipedia Clone](https://github.com/akash-joshi/auth-wiki-public). 

You can also go through the Deno documentation at [deno.land](http://deno.land) to improve your skills.