Build a Custom URL Shortener Using Azure Functions and Cosmos DB

Introduction

This article describes how to build a custom URL shortener service using Azure’s serverless platform with Azure Functions and Cosmos DB. I had this idea after I recently read Jussi Roine’s article, where he built a URL shortener service (such as bit.ly) using a serverless Azure approach, an approach he led with Azure Logic Apps and a custom web app. As I was reading his article, I realized that building the same solution with Azure Functions and proper use of input and output bindings, can yield a highly elegant solution. And so, I embarked on a journey to see just how smart can such a solution be.

I also wanted the solution to be ultra-scalable and achieve very low latencies in response to queries, so I chose to use Azure Cosmos DB as the storage solution. Cosmos DB is Microsoft’s proprietary globally-distributed, multi-model database service. Cosmos DB guarantees less than 10-ms latencies for reads and writes at the 99th percentile, so it was a natural choice.

The Requirements

The URL shortener service should have two endpoints:

  1. A URL registration endpoint, which allows clients to register shortened URLs (often referred to as vanity URLs) with their redirection target.
  2. A URL redirect endpoint, where web browsers can issue GET requests using the vanity domain and receive a redirect to the target URL.

Also, as stated above, the service should possess the following runtime qualities:

  1. Very low latency in response to redirect requests.
  2. Ultra-scalable in terms of both request throughput and geographic scalability.
  3. Relatively cheap to operate, with cost scaling together with request throughput.

The Solution

To answer the above requirements, I decided to proceed with the following solution architecture.

URL Shortener Architecture

Other than meeting the functional needs, this architecture is also aligned with the desired runtime qualities.

  1. Cosmos DB guarantees less than 10-ms latencies for reads (indexed) and writes at the 99th percentile while allowing virtually unlimited throughput (with proper scaling). The only mechanism that can improve this latency is an in-memory cache such as Redis (or Azure Cache for Redis). However, it does require extra work since Redis alone cannot provide the desired persistence consistency. We can combine Redis as a cache mechanism and Cosmos DB for persistence to get the best of both worlds; however, that is out of scope for this article.
  2. We can quickly deploy Cosmos DB in multiple geographies at a click of a button, scaling our data worldwide.
  3. Azure Functions is a lightweight solution with minimal overhead. The only downside to Azure Functions (and virtually any serverless platform) is the cold-start time, which can be very long. However, Premium and Dedicated hosting plans solve this problem on Azure (at the cost of increased financial expense).
  4. Both Cosmos DB and Azure Functions can start cheap, and grow in spending as the required throughput increases.

Cosmos DB

To use Cosmos DB, you can either use the Cosmos DB emulator (on Windows Only) or create a Cosmos DB account on Azure. Cosmos DB offers a free tier that is suitable for our URL shortener. Whichever path you choose, please take note of the Cosmos DB connection string as you’ll need it to configure the Azure Functions we’ll soon implement.

Cosmos DB Emulator

Azure Functions

Creating the Functions App

There are many ways to create an Azure Functions App, and there are multiple supported languages. For my implementation, I chose to use Node.js and JavaScript using Visual Studio Code. To bootstrap the app, you can follow the handy quickstart guide. I used the following inputs in response to the VSCode extension prompts:

  • Language: JavaScript
  • Template: HTTP Trigger
  • Function name: register
  • Authorization level: anonymous (this is great for demo purposes, but for production, you can use either a function key or EasyAuth)

The extension should now create a basic “Hello World”-style Functions app named “register.” You can launch the app by either pressing F5 to debug the app or launching npm start from the terminal window. Both options should launch the Azure Functions runtime and allow you to test your app.

Initial VS Code Workspace

You can test that the function is running properly by executing the following from the workspace’s root folder:

https://gist.github.com/estiller/fb825f6c4de4ee881e5ea7c3deee0415

Once everything works, you can delete the sample.dat file.

Configuring Cosmos DB

To configure Cosmos DB, edit the local.settings.json file and add the following connection string:

https://gist.github.com/estiller/11b30c502fb7d973609f953144ac4a9e

The connection string in the snippet corresponds with the Cosmos DB emulator. If you created a Cosmos DB account in Azure instead, you should replace the connection string with the one available from the portal on your created Cosmos DB instance.

Customizing the Route Prefix

By default, the Azure Functions runtime prefixes all HTTP trigger routes with /api. Since we want to keep the URL short (this is a URL shortener service…), we need to remove this prefix.

Fortunately, the prefix can be customized by editing the host.json file and adding the below configuration.

https://gist.github.com/estiller/78200f8d6f8cfb8093339a86373a3222

This configuration sets the default route prefix to an empty string, effectively removing it.

Authoring the URL Registration Endpoint

First, we’ll configure the function bindings in the function.json file as follows:

https://gist.github.com/estiller/e795ee7283402f1b8343e1bf5b786b6d

Let’s break this definition down to its components:

  1. Lines 4–9 define the HTTP trigger. We’re defining a route that matches POST requests to the /register endpoint. The request is then made available on the req object.
  2. Lines 12–14 define the HTTP response output, assigned from the return object’s property res.
  3. Lines 17–25 are where it gets interesting. These lines define an output binding, which binds the return object’s property registration to a document in a Cosmos DB database collection. As a result, any object that our function assigns to the registration property is automatically placed into the DB without writing any Cosmos DB related code to do so. We will later use this document to redirect incoming requests. In addition, the binding defines that if the database or the collection do not exist, they will be created automatically for us. Pretty neat!

Once all the bindings are in place, the function itself is very straightforward:

https://gist.github.com/estiller/6a6b87334c4e66882c891daac0bee526

We’re expecting a request body with two string properties — url and vanity. If these properties are present, we return a registration object (that gets output into the database via the binding) and a successful HTTP response. Otherwise, a 400 bad request response is returned.

Adding the URL Redirect Endpoint

Once we have the vanity URL registrations in the DB, the final piece is about authoring the redirect logic itself. First, let’s look at the function bindings.

https://gist.github.com/estiller/0481311fa1a46fabc528be64baac2d6d
  1. Lines 4–9 define the HTTP trigger. We’re setting a wildcard route that matches GET requests to any endpoint within the Functions App domain. We’re also capturing the route path in a variable named vanity, which we’ll use later. This section is where part of the magic is happening.
  2. Lines 12–21 are where the magic continues. These lines define a Cosmos DB input binding, which, for each request, automatically fetches the document whose id is vanity. As you recall, this is a variable extracted from the request path, and it matches the identifier used by the output binding in the register function. So, a registration with the vanity URL myvanity generates a document with the id myvanity, which is output into the database. When the redirect is requested by performing a GET on http://domain/myvanity the same document is fetched from the DB, and the function can redirect to it. Sweet and elegant, in my opinion.
  3. Lines 24–26 simply define the HTTP output property name.

Once the bindings are in place, the code itself is also straightforward:

https://gist.github.com/estiller/6e46fe04970161aadde22789ce973765

If a matching document was found in the database, then redirection is sent. Otherwise, a 404 Not Found error is returned.

Testing the Solution

To test the registration process, you can issue the following commands:

https://gist.github.com/estiller/1b7dab59bc8d2628eadd03610c1384cf

Make sure you get a successful 204 No Content response after registration, and a 302 Found in response to the vanity URL request.

Conclusion

Serverless platforms provide a comfortable mechanism for implementing simple APIs in an easy, cost-effective manner. Smartly using Azure Functions bindings allows us to achieve a high degree of sophistication while keeping the binding specific code we need to write to a minimum. Cosmos DB completes the solution as an easy-to-use yet powerful document database platform. Together, we saw how we could utilize these platforms to implement a custom URL shortener service elegantly.

Resources

Here are additional resources:

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *