DEV Community

Cover image for Designing a URL Shortener in Deno
Akash Joshi
Akash Joshi

Posted on

Designing a URL Shortener in Deno

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 (a video-chat platform) or Twitter.

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 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, the "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:

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);
    }
  });
Enter fullscreen mode Exit fullscreen mode

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

{
  "shortcode": {
    "dest": "destination_url_string",
    "expiryDate": "YYYY-MM-DD"
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

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

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

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

console.log(urls);
Enter fullscreen mode Exit fullscreen mode

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:

.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";
    }
  });
Enter fullscreen mode Exit fullscreen mode

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

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";
        }
      });
Enter fullscreen mode Exit fullscreen mode

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.

.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";
  }
});
Enter fullscreen mode Exit fullscreen mode

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. 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:

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();
Enter fullscreen mode Exit fullscreen mode

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:

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";
}
Enter fullscreen mode Exit fullscreen mode

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.

If you need help with anything JS, feel free to reach out through Superpeer (a video-chat platform) or Twitter.

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 or a Wikipedia Clone.

You can also go through the Deno documentation at deno.land to improve your skills.

Top comments (2)

Collapse
 
hanna profile image
Hanna

Why are you doing if(context.params ...) when Deno supports optional chaining? if(urls[context.params?.urlid]) would be sufficient.

Collapse
 
thewritingdev profile image
Akash Joshi

For sure! You're right :)