How to Build a URL Shortener With Go
Time to read: 10 minutes
How to Build a URL Shortener With Go
URL shorteners, such as Bit.ly, are one of the quintessential web apps. They take a long, likely very hard to remember, URL, such as https://github.com/slicer69/sysvinit/releases/tag/3.09, and shorten it, such as https://tinyurl.com/yhxudwwv.
What's more, we've all likely used them whether we realise it or not, such as indirectly through services such as LinkedIn.
While their capabilities vary, however, at their core they do just two things:
Take a long URL and make it shorter, making it easier to read and share
Track the number of times that URL was clicked on
In this tutorial, you're going to learn how to build one with Go.
How will the application work?
When finished, the app will have three routes:
The first will retrieve a list of all of the shortened URLs stored in the application's database and display them, along with a form for shortening URLs.
The second route will process form requests to shorten URLs and store the shortened URL, along with the unshortened URL in the backend, SQLite database.
The third route will retrieve the original URL from the shortened URL and to redirect the user to it.
Prerequisites
To follow along with the tutorial, you will need the following:
A recent version of Go (1.22.0 at the time of writing)
The command line shell for SQLite (version 3)
Your favourite editor or IDE, such as Visual Studio Code or SublimeText
Some prior knowledge of Go would be helpful, though not necessary
We could have used a larger, more feature-rich database, such as PostgreSQL or MS SQLServer. But – especially when starting out – SQLite is a great choice! It's quick to set up, self-contained, cross-platform, and uses a tiny amount of memory.
Let's get building!
Create the project directory structure
The first thing to do is to create the project's directory structure, by running the commands below.
The created directory structure will look like this:
Here's what the directories are for:
templates: This will hold the Go HTML template that will be rendered when the default route is viewed
static/css: This will store the application's sole CSS file
internals/models: This will store the Go source files that provide database interaction support
data: This will store the SQLite database file
Install the required dependencies
Next, let's add Go module support and install the required packages, by running the commands below.
Here's what the packages are for:
Package |
Description |
---|---|
This package simplifies validating that URLs are valid before they're shortened. |
|
This package provides Flash message support, which we'll use to show errors when something goes wrong with shortening a URL. |
|
This package is a fast and lightweight router. We're using it as Go's default router doesn't support restricting routes by HTTP method. |
|
This package adds some extra text manipulation support. |
|
This package provides underlying support for interacting with the app's SQLite database. |
Provision the database
The next thing to do is provision the database. To keep the process as uncomplicated as possible, we're not going to use any dedicated provisioning tools. Rather, we'll run the DDL commands directly using SQLite's command line shell.
Before we can do that, in the data directory, create a new file named load.sql. Then, in that file paste the following.
The first instruction creates a table named urls
. This table stores the shortened and unshortened URLs (original_url
, shortened_url
), along with the number of times that a shortened URL was clicked (clicks
).
The second adds an index on the shortened URL column (idx_shortened
). This improves database performance, as that column is queried the most. The third inserts two records into the table, so that there's something to look at when we test the application, later.
Run the command below to provision the database.
Build the URL shortener
Now, it's time to write some code. Start off by creating a new file in the internals/models directory named urls.go. In the file, past the code below.
The code defines two structs:
ShortenerData
: This stores the records from one table row; the original and shortened URLs and the number of times a shortened URL was clicked.ShortenerDataModel
: This manages the interaction with the SQLite database.
Then, a function, Latest()
is defined on ShortenerDataModel
. This function retrieves all of the records from the url
table, then hydrates and returns an array of ShortenerData
with the retrieved data.
Add the ability to retrieve shortened URLs from the database
Now, let's create the app's default route that both retrieves the shortened URLs from the database and displays them along with a form to shorten URLs.
To do that, in the project's top-level directory create a new file, named main.go. In that file, paste the code below.
The code starts off by defining two structs:
PageData
: this holds data to be passed to the default route's template, such as any errors (Error
), the app's base URL (in our case, http://localhost:8080), and the URL data (URLData
).App
: this models the application. It has one property,urls
, which is a pointer to amodels.ShortenerDataModel
object that, as we've started to see, simplifies database interaction.
Then, it defines the serverError()
function. This is a small utility function for returning an HTTP 500 error, when something serious goes wrong in the application.
Following that, the newApp()
function instantiates a new App
object. The function takes the path to the SQLite database file, opens a connection to it (db
), checks that it works, and returns an initialised App
object.
Then, it defines the formatClicks()
function. This function takes the number of clicks a shortened URL has received and formats it as a string with a thousands separator. It's not strictly necessary, but I thought it makes the table output on the default route that much easier to read.
After that, the function is then added to the list of additional Go template functions. It can be referred to in a Go template with formatClicks
, such as in the following example.
The next method, getDefaultRoute()
, is, likely, the most important as it handles requests to the app's default route (/). The function opens and parses the route's template, templates/default.html, which will be defined next. During this process, the additional list of Go template functions we defined earlier is added by calling Template's Funcs() function.
It then retrieves all of the URL data from the database and stores them in PageData
. Then, the function finishes up by writing the rendered template, along with the URL data, in the response to the request.
Then, another function, named routes()
, is defined on App
. This function defines the app's routing table. The function starts by creating a route to handle requests to the application's static files, stored in the static directory. Then, it defines the default route, setting App
's getDefaultRoute()
to handle requests to it.
Check out httprouter's documentation, if you're not familiar with the package.
Finally, the main()
function is defined. The function instantiates a new App
object, passing it the path to the SQLite database in the data directory. It then instantiates a new HTTP server listening on localhost on port 8080, with the routes defined in App
's routes()
function.
Define the default route's template
We're almost ready to do an initial test of the application. Before we can do that we need to define the Go template which will be rendered and displayed on requests to the default route.
Create a new file in the templates directory named default.html. In that file, paste the code below.
The template is split into two parts. It has a form for shortening URLs, at the top of the page, and a table listing all of the database records at the bottom. It's styled a little, so that it has, I hope, a professional look and feel.
The form contains a single input field named url
, which takes advantage of the URL input type to simplify handling invalid values. As a result, if the user enters a value that isn't identifiable as a URL, your browser will display an error message, avoiding us having to implement one in code.
Otherwise, the form will send the form data to the app's second route (which we'll define shortly), where it will be processed. Clicking on the shortened URLs, opens the app's third route, which redirects the user to the original URL.
I appreciate that the template's quite verbose, as I've used quite a number of Tailwind CSS classes to define the UI. I'm an unabashed fan of the framework as it's made life so much easier building web apps.
Download the CSS file
Next, download the application's CSS file to the static/css directory; saving you the hassle of generating it yourself.
Let's see the application in action!
With the core of the application in place, let's have a look at it. Start it by running the command below.
Then, in your browser of choice, open http://localhost:8080. It should look similar to the screenshot below.
Add the ability to shorten a URL
We can view all of the shortened URLs in the database, so let's add the ability to shorten a URL and store it in the database. To do that, we're going to:
Update the database code
Add a route to process submissions from the URL shortener form in the default template
The first thing we'll do is to add the following function after the existing one in internals/models/urls.go.
This function inserts a new record into the urls
table and returns the number of rows affected, if any. Alternatively, it returns an error if something went wrong while inserting the record.
Next, in main.go add the following code after the imports
list at the top of the file.
Then, update the imports
list to match the following.
After that, in a new terminal session or tab, run the following command, to ensure that the new packages are available.
The first two functions generate a shortened URL, minus the URL scheme. They generate a string of up to 27 random characters, factoring in the current time, and then encode that string.
For complete transparency, I found the functions at https://www.php2golang.com/method/function.uniqid.html.
The third function, setErrorInFlash()
, stores a flash message in the current session. If you're not familiar with flash messages, they're one-time messages that are passed between requests. In our case, we'll store an error message before the user is redirected back to the default route. There, the error message will be displayed in the form, between the input field and submit button.
Next, add the following function in main.go after getDefaultRoute()
.
The function attempts to retrieve the url
parameter from the POST request's body. If the URL was not supplied, or the value supplied was not a valid URL, an applicable error message is flashed to the current session. Then, the user is redirected to the default route, where the message will be displayed.
Otherwise, a shortened URL is generated, then saved to the database along with the original URL. If a new record cannot be created, a message saying that is flashed. Then, the user is redirected to the default route. Otherwise, the user is redirected to the default route, where they'll see the new URL at the top of the URLs table.
Following that, in main.go, in the getDefaultRoute()
function, add the following code in getDefaultRoute()
after the initialisation of pageData
.
These changes retrieve an error message and add it to the template data, if one was flashed while processing the form data.
Finally, in main.go, in the routes()
function, add the code below after the definition of the default route.
This adds a new route to the routing table, the same as the default, but which, when POST requests are made, will be handled by App
's shortenURL()
function.
Add the ability to redirect a shortened URL to the original URL
Now, let's add the third and final aspect of functionality, the ability to redirect a user from a shortened URL to the original URL. To do that, in internals/models/urls.go, add the following two functions at the end of the file.
The first function, Get()
, retrieves and returns the original URL based on the shortened URL supplied. If, however, a record cannot be found matching the shortened URL, a customised Errors object, ErrNoRecord
, which we'll see shortly, is returned instead.
The second function, IncrementClicks()
, updates the urls
table. It increments the value in the clicks
column for the record containing the shortened URL.
After that, update the imports
list to match the following.
Next, in main.go paste the code below after shortenURL()
.
This retrieves the shortened URL from the request and uses it to retrieve the original URL from the database. If something goes wrong, an HTTP 500 error is returned. Otherwise, the code attempts to increment the clicks for the shortened URL. As before, if something goes wrong, an HTTP 500 error is returned. Assuming that the original URL is returned, the user is redirected to it.
Finally, in main.go, in the routes()
function, add the code below after the definition of the second route.
This defines a new route accessible only with GET requests, that will be handled by the openShortenedRoute()
function. The route's path contains a named parameter, :url
, containing the shortened URL.
Test that the URL shortener works as expected
That's it. The application now has all of its functionality. So, let's test it. Stop the existing Go process, and restart it by running the command below.
Then, open http://localhost:8080 in your browser of choice. After that, shorten a valid URL, such as https://www.thenewdaily.com.au/. You should see the URL added to the bottom of the list with its clicks set to zero. Clicking on a URL in the Shortened URL column will redirect you to the original URL.
Now, click Shorten URL without entering a URL. You should see an error message appear below the URL text field, as in the screenshot below.
Now, if you enter an invalid URL and click Shorten URL, you'll see the following error appear.
And that's how to build a simple URL shortener in Go
While it's not the most feature-rich implementation of a URL shortener, it shows the essential functionality. What would you add or change?
Would you add record pagination? Would you limit the number of records returned? Would you cache the query results?
Granted, it's not that common to build web apps completely in Go. It's far more common to implement the frontend with a framework, such as Svelte or React. But, for the purposes of a simplistic example, there's no harm in simplifying the technology stack. Have a look at the GitHub repository if you'd like to see the entire code.
Matthew Setter is a PHP Editor in the Twilio Voices team and a PHP, Go, and Rust developer. He’s also the author of Deploy With Docker Compose. When he's not writing PHP code, he's editing great PHP, Go, and Rust tutorials here at Twilio. You can find him at msetter[at]twilio.com, and on LinkedIn, Twitter, and GitHub.
Related Posts
Related Resources
Twilio Docs
From APIs to SDKs to sample apps
API reference documentation, SDKs, helper libraries, quickstarts, and tutorials for your language and platform.
Resource Center
The latest ebooks, industry reports, and webinars
Learn from customer engagement experts to improve your own communication.
Ahoy
Twilio's developer community hub
Best practices, code samples, and inspiration to build communications and digital engagement experiences.