In this article, we’re going to learn the basics of Deno, like how to run a program and embrace security. If you need help with anything JS, feel free to reach out through Superpeer (a video-chat platform) or Twitter.
Deno is the new JavaScript and TypeScript runtime written in Rust. It offers tight security, TypeScript support out-of-the-box, a single executable to run it, and a set of reviewed and audited standard modules.
Like npm in Node.js, packages in Deno are managed in a centralized package repository called X. We'll be using one of these libraries, Oak, to build a REST API-based server in Deno.
After learning the basics by using the Express-like router package Oak, we will jump into the deep end of Deno and build a complete application.
Here's what we will set up in this application:
1. Mapping URL shortcodes to endpoints using a JSON-based config file.
2. Have expiration dates attached to each URL so that these redirects are only valid for a limited period of time.
1. Install Deno from this link.
2. Make sure you know the basics of JavaScript.
Although not really required to follow along with this article, you can check
out the YouTube video at the top to get an intro to Deno in video-format.
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);
}
});
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.
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"
}
}
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);
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";
}
});
The if condition in the route does the following:
- Checks if parameters are attached to the route.
- Checks if the parameter urlid is in the parameter list.
- 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";
}
});
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.
To make the urls object react live to changes in the JSON
file, we simply move the read statement inside our route. This should
look like the following:
.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";
}
});
Note how we have moved the URLs object inside our router. Now in this
case, the config file is read every time that route is called, so it can react live to any changes made in the urls.json file. So even if we add or remove other redirects on the fly, our code reacts to it.
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();
To check the date for when the URL will expire, we check the expiryDate key on our urls object. This will make the code 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";
}
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.
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.
If you need help with anything JS, feel free to reach out through Superpeer (a video-chat platform) or Twitter, or my website at akashj.com.