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:
- A URL registration endpoint, which allows clients to register shortened URLs (often referred to as vanity URLs) with their redirection target.
- 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:
- Very low latency in response to redirect requests.
- Ultra-scalable in terms of both request throughput and geographic scalability.
- 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.
Other than meeting the functional needs, this architecture is also aligned with the desired runtime qualities.
- 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.
- We can quickly deploy Cosmos DB in multiple geographies at a click of a button, scaling our data worldwide.
- 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).
- 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.
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.
You can test that the function is running properly by executing the following from the workspace’s root folder:
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:
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.
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:
Let’s break this definition down to its components:
- 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 thereq
object. - Lines 12–14 define the HTTP response output, assigned from the return object’s property
res
. - 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 theregistration
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:
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.
- 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 namedvanity
, which we’ll use later. This section is where part of the magic is happening. - 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 theregister
function. So, a registration with the vanity URLmyvanity
generates a document with the idmyvanity
, which is output into the database. When the redirect is requested by performing aGET
onhttp://domain/myvanity
the same document is fetched from the DB, and the function can redirect to it. Sweet and elegant, in my opinion. - Lines 24–26 simply define the HTTP output property name.
Once the bindings are in place, the code itself is also straightforward:
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:
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: