paint-brush
How to Build a URL Shortener in Go: A Short Guideby@4rkal
141 reads

How to Build a URL Shortener in Go: A Short Guide

by 4rkalOctober 8th, 2024
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

In this article, I will be going through how to make a URL shortener in Go. The final result will look something like this shortr, source code.
featured image - How to Build a URL Shortener in Go: A Short Guide
4rkal HackerNoon profile picture

In this article, I will be going through how to make a URL shortener in Go. The final result will look something like this shortr, source code.

This is a great weekend project; especially if you’re new to go.

What Is a URL Shortener?

A URL shortener is a tool that takes a long URL and shrinks it down into something much shorter and easier to share. Instead of copying and pasting a long string of letters, numbers, and symbols, you get a compact version that leads to the same destination. For example, a long URL like www.somelongwebsite.com/articles/this-is-a-super-long-link could become something like bit.ly/abc123. It’s super handy for sharing links on social media, in texts, or anywhere space is limited. And most URL shorteners provide analytics like link clicks.

Requirements

  1. Go installed on your system.
  2. A code editor, eg vs code, neovim


In this project, I will be using echo as the HTTP server and the standard HTML library.

Project Setup

Create a new directory to house our project

mkdir project-name
cd project-name


Assuming you have Golang installed.


Create a new go module (project):

go mod init project-name


Before we start writing any code, we first have to install echo:

go get github.com/labstack/echo/v4


Now, create a new file called main.go

touch main.go

And open it in your favorite editor.

Creating URL Handlers

func main() {

	e := echo.New()
	
	e.Use(middleware.Logger())
	e.Use(middleware.Recover())
	e.Use(middleware.Secure())
	
	e.GET("/:id", RedirectHandler)
	e.GET("/", IndexHandler)
	e.POST("/submit", SubmitHandler)
	
	e.Logger.Fatal(e.Start(":8080"))

}

This will create three different routes/handlers.

The /:id, which will redirect the user to the required website.


The / which will display a URL submission form for new URLs to be added.


Finally, the /submit which will handle URL submissions from the form in /

Redirect Handler

The most important part of our application is the redirect handler, which will redirect the user to the URL that was specified.

Before we create any URLs, we first have to declare some variables and make a helper function.


In order to have a random ending to our URL, e.g., /M61YlA, we will create a new function called GenerateRandomString

const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

func generateRandomString(length int) string {
	seededRand := rand.New(rand.NewSource(time.Now().UnixNano()))
	
	var result []byte
	
	for i := 0; i < length; i++ {
	index := seededRand.Intn(len(charset))
	result = append(result, charset[index])
	}
	
	return string(result)

}

This will select length random characters from the charset. If you want your slugs (urls), to not contain any capital letters, you can remove them from the charset.


Now, we will need to have a place to store all of our links. In this example, we will be storing them in memory and not a database.


Create a new struct called Link and a map called LinkMap:

type Link struct {
	Id string
	Url string
}

var linkMap = map[string]*models.Link{}


You can also add some sample data to it.

var linkMap = map[string]*Link{ "example": { Id: "example", Url: "https://example.com", }, }


Now, we can (finally) create our RedirectHandler, which will handle all of the redirects for our URL shortener.

func RedirectHandler(c echo.Context) error {
	id := c.Param("id")
	link, found := linkMap[id]
	
	if !found {
	return c.String(http.StatusNotFound, "Link not found")
	}

	return c.Redirect(http.StatusMovedPermanently, link.Url)
}

This function will get the id of the link, e.g., /123 and will look for it in the global LinkMap; if it is not available, it will return an error that the link was not found. Otherwise, it will redirect the user to the specified URL using a 301 Permanently Moved HTTP response code.

Recap #1

The code so far should look something like this:

package main

import (
"math/rand"
"time"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)

type Link struct {
	Id string
	Url string
}

const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

var linkMap = map[string]*Link{ "example": { Id: "example", Url: "https://example.com", }, }

func main() {
	e := echo.New()
	
	e.Use(middleware.Logger())
	e.Use(middleware.Recover())
	e.Use(middleware.Secure())
	
	e.GET("/:id", RedirectHandler)
	//e.GET("/", IndexHandler)
	//e.POST("/submit", SubmitHandler)
	
	e.Logger.Fatal(e.Start(":8080"))
}

func RedirectHandler(c echo.Context) error {
	id := c.Param("id")
	link, found := linkMap[id]
	
	if !found {
	return c.String(http.StatusNotFound, "Link not found")
	}
	
	return c.Redirect(http.StatusMovedPermanently, link.Url)
}

func generateRandomString(length int) string {
	seededRand := rand.New(rand.NewSource(time.Now().UnixNano()))
	
	var result []byte
	
	for i := 0; i < length; i++ {
	index := seededRand.Intn(len(charset))
	result = append(result, charset[index])
	}
	
	return string(result)

}


Run the server.

go run .


You might also want to install any missing dependencies:

go mod tidy


If you head to localhost:8080/example, you should be redirected to example.com

Submission Handlers

We will now define two new routes inside of our main function

e.GET("/", IndexHandler)
e.POST("/submit", SubmitHandler)

These two handlers will handle the default page displayed in / which will contain a form that will be submitted to /submit in a post request.

For the IndexHandler, our code will look something like this:

func IndexHandler(c echo.Context) error {
	html := `
		<h1>Submit a new website</h1>
		<form action="/submit" method="POST">
		<label for="url">Website URL:</label>
		<input type="text" id="url" name="url">
		<input type="submit" value="Submit">
		</form>
		<h2>Existing Links </h2>
		<ul>`
	
	for _, link := range linkMap {
		html += `<li><a href="/` + link.Id + `">` + link.Id + `</a></li>`
	}
	html += `</ul>`
	
	return c.HTML(http.StatusOK, html)
}

When we visit / a submission for will be rendered, to submit a new website. Under the form, we will see all registered links from our Linkmap

P.S. it is not recommended that you use HTML like this. You should be separating the html file or using a library like temple.


The submission handler SubmitHandler should look something like this

func SubmitHandler(c echo.Context) error {
	url := c.FormValue("url")
	if url == "" {
		return c.String(http.StatusBadRequest, "URL is required")
	}
	
	if !(len(url) >= 4 && (url[:4] == "http" || url[:5] == "https")) {
		url = "https://" + url
	}
	
	id := generateRandomString(8)
	
	linkMap[id] = &Link{Id: id, Url: url}
	
	return c.Redirect(http.StatusSeeOther, "/")
}

This handler will take a URL from the form that was submitted, do some (simple) input validation, and then append it to the linkMap.

Final Recap

The code for our URL shortener is:

package main

import (
	"math/rand"
	"net/http"
	"time"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

  

type Link struct {
	Id string
	Url string
}

const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

var linkMap = map[string]*Link{"example": {Id: "example", Url: "https://example.com"}}

  
func main() {

	e := echo.New()
	
	e.Use(middleware.Logger())
	e.Use(middleware.Recover())
	e.Use(middleware.Secure())
	
	e.GET("/:id", RedirectHandler)
	e.GET("/", IndexHandler)
	e.POST("/submit", SubmitHandler)
	
	e.Logger.Fatal(e.Start(":8080"))
}

  
func generateRandomString(length int) string {
	seededRand := rand.New(rand.NewSource(time.Now().UnixNano()))

	var result []byte 
	
	for i := 0; i < length; i++ {
		index := seededRand.Intn(len(charset))
		result = append(result, charset[index])
	}

	return string(result)
}

  
func RedirectHandler(c echo.Context) error {
	id := c.Param("id")
	link, found := linkMap[id]

	if !found {
		return c.String(http.StatusNotFound, "Link not found")
	}

	return c.Redirect(http.StatusMovedPermanently, link.Url)
}

  
func IndexHandler(c echo.Context) error {
	html := `
		<h1>Submit a new website</h1>
		<form action="/submit" method="POST">
		<label for="url">Website URL:</label>
		<input type="text" id="url" name="url">
		<input type="submit" value="Submit">
		</form>
		<h2>Existing Links </h2>
		<ul>`
	
	for _, link := range linkMap {
		html += `<li><a href="/` + link.Id + `">` + link.Id + `</a></li>`
	}
	html += `</ul>`
	
	return c.HTML(http.StatusOK, html)
}

  

func SubmitHandler(c echo.Context) error {
	url := c.FormValue("url")
	if url == "" {
		return c.String(http.StatusBadRequest, "URL is required")
	}
	
	if !(len(url) >= 4 && (url[:4] == "http" || url[:5] == "https")) {
		url = "https://" + url
	}
	
	id := generateRandomString(8)
	
	linkMap[id] = &Link{Id: id, Url: url}
	
	return c.Redirect(http.StatusSeeOther, "/")
}

Closing Words

This is a great small project if you are new to/learning Go.

It can be very helpful if you extend beyond this tutorial. For example, here are some other ideas that you can add to the project:

  1. Enhance the input validation
  2. Track link clicks + Statistics Page
  3. Improve UI (HTML)
  4. Dockerizing the application
  5. ++


I did all of those and my URL shortener (called shortr) can be accessed under the URL app.4rkal.com, and the source code is here

Join My Mailing List

Subscribe here: https://newsletter.4rkal.com/subscription/form