left-icon

Deno Succinctly®
by Mark Lewin

Previous
Chapter

of
A
A
A

CHAPTER 3

Create a URL Shortener Console Application

Create a URL Shortener Console Application


In this chapter, we’ll build a URL shortener using Deno and turn it into a self-contained console application called surl that you can run in your terminal. It should allow you to enter a URL and return both the original URL and the new, shortened version:

Code Listing 30

$ surl https://deno.land/manual/tools/dependency_inspector
SUCCESS: Your long URL: https://deno.land/manual/tools/dependency_inspector

... is now shorter: https://cleanuri.com/M06pJb

Our application will use the cleanuri API to do the actual shortening, and Deno tools to install it as a single executable. We’ll also provide a logging facility, so that you can see a record of the original URLs and their shortened versions.

Note: We’ll use this example as a demonstration of some of Deno’s features. But, for the most part, if you’re familiar with JavaScript (and especially with TypeScript), I suspect that you’ll feel right at home here. If TypeScript is entirely new to you, then I recommend that you read TypeScript Succinctly, which you can download for free here.

Creating your project

In your favorite code editor or IDE, create a folder called surl. Within that folder, create two files: a mod.ts file for your code, and a urls.txt file with which to log your original URLs and their shortened counterparts.

Why mod.ts and not index.ts or similar? If you’ve come from the world of Node, then you’ll know that the index.js file is special in that it specifies the default entry point for your application. This is especially important when you require Node modules.

When you pass a directory to Node’s require function, then it has some work to do to figure out how to run the module that you have specified. First, it checks package.json for an endpoint. If that isn’t defined, it then looks for the presence of index.js. If it doesn’t find an index.js file, it makes a last-ditch attempt to find index.node (a C++ extension format).

You can see that in the absence of a package.json entry, the most likely entry point for your code is index.js—hence the importance of that name in Node.

Ryan Dahl has said that placing such great importance on the index.js file as the default entry point for a module’s code is a major regret. It added a lot of complexity to the module loading system. For this reason, Deno doesn’t care what you call your main code file. In fact, you must explicitly name this file as part of your import statement. For example, to use the csv module in the standard API:

Code Listing 31

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

Having said that though, developers like to standardize wherever possible, so you’ll find that a lot of Deno code uses mod.ts as the entry point—and we’ll do the same in this book. This is not always the case, however, especially for third-party modules, so please bear that in mind.

Note: You can listen to Ryan Dahl lamenting his choices behind index.js in his talk, 10 Things I Regret About Node.js.

Accepting command-line arguments

For our tool to be useful, we need to provide a way for the user to enter the URL they want to shorten as a command-line argument.

For this, as in so many other common use cases, Deno has us covered. We can use the Deno.args variable in the runtime API to see which arguments are passed to our command-line program.

But wait a minute. What’s that Deno namespace? To answer that question, let’s have a look at how the runtime API is structured.

The Deno runtime consists of web APIs and the Deno global namespace. Deno wants you to use familiar APIs wherever possible, so where a web standard exists (such as fetch for HTTP requests), Deno supports these rather than forcing you to use a new proprietary API.

Deno's web APIs

Figure 5: Deno's web APIs

All the APIs that Deno supports that are not web standard are contained in the Deno global namespace. These include APIs for reading from files, opening TCP sockets, and many operations that you, as a developer, are likely to use often.

The Deno global namespace

Figure 6: The Deno global namespace

So, back to accepting command-line arguments. The Deno global namespace has a variable called Deno.args, which accepts an array of strings, one for each argument passed to your program. We can test it out as shown in the following code.

In mod.ts, include this single line of code:

Code Listing 32

console.log(Deno.args);

Run it with deno run mod.ts firstArgument secondArgument. It should return an array containing the two arguments passed to it:

Code Listing 33

$ deno run mod.ts firstArgument secondArgument

Check file://.../surl/mod.ts

[ "firstArgument", "secondArgument" ]

Validating the URL

Now we know how to accept a URL argument to our shortener. The next thing we need to worry about is whether the URL supplied is a valid one. We need a regular expression for that. Thankfully (because of my rather lacking regex skills) a suitable expression is easy to find on the web:

Code Listing 34

^(https?://)?(www\\.)?([-a-z0-9]{1,63}\\.)*?[a-z0-9][-a-z0-9]{0,61}[a-z0-9]\\.[a-z]{2,6}(/[-\\w@\\+\\.~#\\?&/=%]*)?$

We just need to wrap it up in a function and pass our submitted URL to it:

Code Listing 35

const { args: [url] } = Deno;

function isUrlValid(input: string) {

  const regex =

    "^(https?://)?(www\\.)?([-a-z0-9]{1,63}\\.)*?[a-z0-9][-a-z0-9]{0,61}[a-z0-9]\\.[a-z]{2,6}(/[-\\w@\\+\\.~#\\?&/=%]*)?$";

  var url = new RegExp(regex, "i");

  return url.test(input);

}

function shorten(url: string) {

  if (url === "" || url === undefined{

    console.error("No URL provided");

    return;

  }

  if (!isUrlValid(url)) {

    console.error(`${url} is invalid`);

  } else {

    console.log(`${url} is a valid URL`);

  }

}

shorten(url);

Tip: The string enclosed by backticks in the console.log and console.error statements is an ES6 feature that enables string interpolation.

If we run this code, we should be able to enter any URL and have our program determine whether it is valid or not:

Code Listing 36

$ deno run mod.ts https:www.bbc.co.uk

https:www.bbc.co.uk is invalid

$ deno run mod.ts https://www.bbc.co.uk

https://www.bbc.co.uk is a valid URL


$ deno run mod.ts

No URL provided

There’s nothing particularly Deno-esque going on there. In fact, the only reference to anything Deno is the Deno.args global environment variable that we’re using to accept a URL as input.

In this example, however, we are using the destructuring assignment syntax in ECMAScript 6 to access Deno.args. This syntax enables you to unpack values from arrays, or properties from objects, into distinct variables. Here, we’re only interested in the first element in the Deno.args array, which we call url, so we unpack that accordingly:

Code Listing 37

const { args: [url] } = Deno;

If we include extra arguments when we execute the program, they are ignored:

Code Listing 38

$ deno run mod.ts https://www.bbc.co.uk https://google.com https://slashdot.org

https://www.bbc.co.uk is a valid URL

Shortening the URL

Now that we can pass a URL into our program, we want to submit it to a third-party API to shorten it for us. I favor cleanuri for this, at least for demonstration purposes, because it doesn’t require any signup, API key, or secret to use.

Note: The cleanuri API docs are here.

Let’s refactor the shorten() method in our code to submit any valid URL our program receives to cleanuri.com for shortening:

Code Listing 39

async function shorten(url: string): Promise<{ result_url: string }> {

  if (url === "" || url === undefined{

    throw { error: "No URL provided" };

  }

  if (!isUrlValid(url)) {

    throw { error: "The URL provided is invalid" };

  }

  const options = {

    method: "POST",

    headers: {

      "Content-Type": "application/json",

    },

    body: JSON.stringify({ url }),

  };

  const res = await fetch("https://cleanuri.com/api/v1/shorten", options);

  const data = await res.json();

  return data;

}

try {

  const { result_url } = await shorten(url);

  console.log(`SUCCESS: Your long URL: ${url}`);

  console.log(`... is now shorter: ${result_url} `);

} catch (error{

  console.log(`ERROR: ${error}`);

}

If we run it, everything should work nicely:

Code Listing 40

$ deno run --allow-net mod.ts "https://www.bbc.co.uk"

SUCCESS: Your long URL: https://www.bbc.co.uk

... is now shorter: https://cleanuri.com/x60ZOd

Tip: Now that the program wants to access an external API over the internet, you must grant it network access. Make sure that you include the -–allow-net flag, or it won’t work!

As you can see, we’re using a standard fetch to make a POST request to cleanuri with our JSON-formatted URL as the payload. Because we don’t want the rest of our code to execute before we’ve got the results back from the API call, we use await and declare the surrounding shorten function as async, and its return type as Promise.

We then call surl asynchronously too, with another await. But hold on a moment! We know that the async/await paradigm in JavaScript requires that any function that uses an await should be marked as async. How have we gotten away with it here?

This is another cool feature of Deno: top-level await, which we encountered briefly back in Chapter 1.

Top-level await

At the time of writing, this is something that even Node cannot do, even though support for it has been added to the V8 engine. There are workarounds which require Node developers to use immediately invoked functions tagged as async, but this is messy.

With Deno, the async declaration in your top-level code is implicit, which leads to much tidier code overall. It means that you can perform asynchronous tasks such as connect to a database, make an API request, and so on from your startup code while your application loads—without having to wrap those operations in an async function just to get them to work. It also makes it easy to check for missing dependencies and fall back gracefully:

Code Listing 41

let myModule = null;

try {

    myModule = await import('https://url1');

} catch (exception{

    myModule = await import('https://url2');

}

Logging

There’s just one thing left to do: start logging the URLs submitted to surl and their shortened counterparts. We just want to write these to a local text file. That’s a common requirement, so our first port of call should be to the Deno runtime API documentation to see if there’s anything there that can help us.

You’ll see that there are a number of different functions for reading from and writing to files. Deno.writeTextFile() looks perfect for our needs:

The Deno.writeTextFile() function

Figure 7: The Deno.writeTextFile() function

All we need to do is invoke that function at the end of the try clause in our try/catch block, passing in the name of the file to write to (urls.txt: put it in your HOME directory to make it easy to locate), and the original (url) and shortened (result_url) URLs:

Code Listing 42

try {

  const { result_url } = await  shorten(url);

  console.log(`SUCCESS: Your long URL: ${url}`);

  console.log(`... is now shorter: ${result_url} `);

  await Deno.writeTextFile(

    "~/urls.txt",

    `${url} -> ${result_url}\n`,

    { append: true },

  );

} catch (error{

  console.log(`ERROR: ${error}`);

}

Now, when we run the application, we should see those URLs logged to urls.txt:

Code Listing 43

$ deno run --allow-net mod.ts "https://www.slashdot.org"

Check file://.../surl/mod.ts

SUCCESS: Your long URL: https://www.slashdot.org

... is now shorter: https://cleanuri.com/5mkmpd

ERROR: PermissionDenied: write access to "urls.txt", run again with the --allow-write flag

Oops! Now that we’re asking our Deno program to write to the file system, we need to give it the appropriate permission to do so:

Code Listing 44

$ deno run --allow-net --allow-write mod.ts "https://www.slashdot.org"

SUCCESS: Your long URL: https://www.slashdot.org

... is now shorter: https://cleanuri.com/5mkmpd

And then we can examine the log entry in urls.txt:

Code Listing 45

https://www.slashdot.org -> https://cleanuri.com/5mkmpd

Deploying our console application

Now that we’ve created a useful tool, let’s package it up so that we can share it. Nobody wants to execute deno run --allow-net --allow-write mod.ts "https://www.example.com" every time they want to shorten a URL.

To do this, we’ll use the deno install command to create a script that will run our code with the correct permissions.

There are a couple of different ways of doing this, depending on where you want to install your script, and what name you want to give it.

In our example, we can just run the following from within our code directory:

Code Listing 46

$ deno install --allow-net --allow-write mod.ts

This will create a script called surl (based on the directory where mod.ts is located) in the .deno/bin folder, in your computer’s home directory. If you examine the contents of that file, you will see the following:

Code Listing 47

% generated by deno install %

@deno.exe "run" "--allow-write" "--allow-net" "file:///<path>/surl/mod.ts" %*

In this instance, deno install looked at the name of the TypeScript file, saw it was just a generic mod.ts file, and decided to use the name of the parent directory instead (surl).

If you then add that script to your PATH, you can run it anywhere on your machine as follows:

Code Listing 48

$ surl https://www.amazon.com

Tip: deno install uses the name of the parent directory for the script if the file you referred to is called either main, mod, index, or cli. By default, it installs to ~/.deno/bin. You can read the full documentation for deno install here

You can also override the default script name by using the --name (or -n) flag:

Code Listing 49

$ deno install --allow-net --allow-write -n myexec mod.ts

Summary

Congratulations! You built a useful little command-line tool in Deno. Along the way you learned about:

  • Top-level await.
  • Writing to a CSV file.
  • Distributing executable code with deno install.

You can find the complete source code here.

Scroll To Top
Disclaimer
DISCLAIMER: Web reader is currently in beta. Please report any issues through our support system. PDF and Kindle format files are also available for download.

Previous

Next



You are one step away from downloading ebooks from the Succinctly® series premier collection!
A confirmation has been sent to your email address. Please check and confirm your email subscription to complete the download.