Building a custom URL shortener service using Azure and a Serverless approach
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!