Building a custom URL shortener service using Azure and a Serverless approach

Photo by @brookelark / Unsplash.com

Summer in Finland was two weeks ago. Now it’s dull, gray, cold, and windy. So the perfect time to build yet another solution using Azure!

This time, I wanted to build a custom URL shortener service. Microsoft has one, and it’s called aka.ms. Another one is Twitter’s t.co, and one that I see many people using is bit.ly. The idea is that you can alias a website – say, https://jussiroine.com would be accessible via https://bit.ly/jussi, for example. You can of course add complex URL parameters, to avoid some headaches. Sometimes the shortened URLs are called vanity URLs or vanity domains.

I wrote about harvesting aka.ms vanity URLs previously here.

The components of a URL shortener service are:

  • A tool to add and store mappings – URL and vanity URL (the shortened address)
  • A web page to accept calls to the vanity URL, look up the mappings and redirecting to the real destination

Pretty simple! But turns out – again – that building simple things often leads to discovering different challenges, and managing through those.

Designing the solution

As is customary, I want to avoid using virtual machines. They add unnecessary complexity. When ideal, I also like to avoid writing tons of code. With custom code, you can admittedly tackle any challenge, but you introduce so many moving parts, at times it’s not reasonable.

I wanted to use .NET Core to future-proof my solution. I set out to design the solution as follows:

  • Storing my vanity URLs and mappings I chose to use an Azure SQL database – it’s super simple, affordable, and widely supported.
  • Adding new URLs to my vanity URL database I chose to create a simple command-line tool using .NET Core. This tool calls into a Logic App that takes care of parsing the input.
  • For redirecting the vanity URL requests to real URLs I chose to create a simple ASP.NET Core Web App with some redirection magic.

I thus needed to create the command-line tool for adding new mappings, a Logic App to process them, and a Web App to act as the landing site for my URL shortening service.

Next, let’s build each of these in order.

Creating the Azure SQL database

This was easy. I provisioned a new Azure SQL Server in Azure and created a single database. The pricing tier is Basic, which translates to ‘cheap’, yet performant enough for my needs.

I created the following SQL table structure:

Creating the command-line tool to add new vanity URL mappings

Next, it’s time to create a command-line tool. This is useful for quickly testing the service later on, and can be easily ported to other platforms when needed.

I chose to use .NET Core, as it is so versatile.

First, I defined a class object to hold my vanity data:

public class vanityObject
{
	public string url { get; set; }
	public string vanity { get; set; }
	public string submittedBy { get; set; }
}

url is the true URL, vanity is the nice-looking alias for it, and submittedBy is to keep track who submitted the new entry.

And now, the main program – it’s relatively easy so I’ll just lay it out first:

static void Main(string[] args)
{
	vanityObject vanityObj = new vanityObject();

	vanityObj.url = args[0];
	vanityObj.vanity = args[1];
	vanityObj.submittedBy = "shortURLCLI"; 

	string jsonString;
	jsonString = JsonSerializer.Serialize(vanityObj);

	string uri = "URL-TO-LOGIC-APPS";

	HttpClient client = new HttpClient();

	StringContent content = new StringContent(jsonString, System.Text.Encoding.UTF8, "application/json");

	client.DefaultRequestHeaders.Accept.Clear();
	client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
	var response = client.PostAsync(uri, content);

	string returnContent = response.Result.Content.ReadAsStringAsync().Result;

	Console.WriteLine($"{returnContent}");

}

I create a new object with my data (passed on as parameters from the command line) and serialize that with help from System.Text.Json. See my previous article on using it.

Then, I’ll perform a HTTP POST to my API, which I’ll create shortly using Azure Logic Apps. And assuming all goes well, I’ll print out the HTTP/200 OK from the API.

Here is an example of running the tool, to add Google with a vanity URL of ‘g’:

In essence, the command-line utility simply calls an API and passes on the data in a JSON payload. The payload the API is expecting is:

{
    "properties": {
        "submittedby": {
            "type": "string"
        },
        "url": {
            "type": "string"
        },
        "vanity": {
            "type": "string"
        }
    },
    "type": "object"
}

Building the Logic App

Moving on to the Logic App. This is the API the command-line utility calls that stored our vanity URL and mapping data in the Azure SQL. I chose to use a Logic App, as it’s such a rapid way to prototype. An Azure Function would perhaps be slightly faster, but less maintainable at the same time.

First, this is how the completed Logic App looks:

It’s HTTP triggered, and expecting the JSON payload. Once it gets that, it populates three variables to make it easier to work with the data:

And then, it’s a simple matter of submitting that to the database:

Executing this workflow takes about 800 ms, so relatively fast.

Querying the database reveals that mappings have been stored successfully:

Building the web app

Adding new vanity URLs and their mapping data works now end-to-end – from the command-line utility to the database, with help from the Logic App. What is left now is a simple Web App, that redirects users.

The idea is to expose a single Web App, such as https://shorturl2020.azurewebsites.net/, and any calls to that would be picked up. If a user opens https://shorturl2020.azurewebsites.net/ex1, then ex1 is the vanity URL that needs to be resolved (to https://example.com, as seen in the database snapshot above, see vanity_id == 2).

I first created the redirection logic. As I’m using ASP.NET Core, I knew I could rely on web.config for this. It took a bit of fiddling, but I ended up with this rule:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <location path="." inheritInChildApplications="false">
    <system.webServer>
      <rewrite>
            <rules>
                <rule name="Vanity">
                     <match url="(.*)" />
                         <conditions logicalGrouping="MatchAll">
                             <add input="{R:0}" negate="true" pattern="^Index*." ignoreCase="true" />
                        </conditions>
                          <action type="Redirect" url="/Index?vanity={R:0}" appendQueryString="false" redirectType="Permanent" />
                </rule>
            </rules>
        </rewrite>
</configuration>

It matches all URLs (.*) but exempts any URLs beginning with /Index. This is my redirection page. All other URLs I assume, are vanity URLs that must be resolved. So I’ll pick up the desired URI part and redirect the user to https://shorturl2020.azurewebsites.net/Index?vanity={parameter}.

Now that this critical piece is completed, let’s take a look at the Web App. It’s ASP.NET Core, and has very little complexity in it.

In Index.cshtml.cs, which is the code file for the default Index page, I added this in the OnGet() method:

StringValues queryVal;

	bool v = HttpContext.Request.Query.TryGetValue("vanity", out queryVal);
	if (v)
	{
		string vanity = queryVal.FirstOrDefault();

		string pattern = @"^[a-zA-Z0-9]*$";
		Regex regex = new Regex(pattern);
		Match match = regex.Match(vanity);

		if (match.Success)
		{
			SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder();

			builder.DataSource = "AZURE-SQL-SERVER";
			builder.UserID = "DBADMIN";
			builder.Password = "PASSWORD";
			builder.InitialCatalog = "DATABASE";

			using (SqlConnection connection = new SqlConnection(builder.ConnectionString))
			{
				connection.Open();
				StringBuilder sb = new StringBuilder();
				sb.Append("SELECT destination_URL FROM vanityurls WHERE vanity_URL='" + vanity + "'");
				String sql = sb.ToString();

				using (SqlCommand command = new SqlCommand(sql, connection))
				{
					using (SqlDataReader reader = command.ExecuteReader())
					{
						while (reader.Read())
						{
							Response.Redirect(reader.GetString(0));
						}
					}
				}
			}
		}
	}

I’m first checking if a query string (?vanity={ID}) exists. I’ll then do a quick RegEx match to filter out anything that shouldn’t be there – like SQL injections. And finally, I’ll do a query against the database, and get the vanity URL – and perform a redirection.

Does it work?

See for yourself:

In conclusion

This build consisted of a few moving parts. Admittedly, the trickiest was the web.config redirection, as I haven’t touched those in almost a decade now. It’s rudimentary, in that it doesn’t reveal too much logic outside the basic vanity ID, but it’s a nice skeleton that I can use to build a better version in the future.

The Azure SQL database is the one thing that actually costs a little bit of money – all other components are either running locally or utilizing a free tier in Azure. The Logic App will eventually cost money also, but for now, it’s not used enough to warrant any worries.

I feel it’s highly useful to learn about different frameworks, programming languages, and platforms by building solutions like this – even if certain parts of the solution are very bare-bones!