How to build a URL shortener in Go

4rkal - Oct 8 - - Dev Community

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

Enter fullscreen mode Exit fullscreen mode

Assuming you have golang installed.

Create a new go module (project):

go mod init project-name

Enter fullscreen mode Exit fullscreen mode

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

go get github.com/labstack/echo/v4

Enter fullscreen mode Exit fullscreen mode

Now create a new file called main.go

touch main.go

Enter fullscreen mode Exit fullscreen mode

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"))

}

Enter fullscreen mode Exit fullscreen mode

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. eg /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)

}

Enter fullscreen mode Exit fullscreen mode

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{}

Enter fullscreen mode Exit fullscreen mode

You can also add some sample data to it.

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

Enter fullscreen mode Exit fullscreen mode

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)
}

Enter fullscreen mode Exit fullscreen mode

This function will get the id of the link eg /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)

}

Enter fullscreen mode Exit fullscreen mode

Run the server

go run .

Enter fullscreen mode Exit fullscreen mode

You might also want to install any missing dependencies:

go mod tidy

Enter fullscreen mode Exit fullscreen mode

If you head tolocalhost: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)

Enter fullscreen mode Exit fullscreen mode

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="https://4rkal.com/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="https://4rkal.com/` + link.Id + `">` + link.Id + `</a></li>`
    }
    html += `</ul>`

    return c.HTML(http.StatusOK, html)
}

Enter fullscreen mode Exit fullscreen mode

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

PS it is not recommended that you use html like this. You should be separating the html file or using a library like templ.

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, "/")
}

Enter fullscreen mode Exit fullscreen mode

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="https://4rkal.com/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="https://4rkal.com/` + 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, "/")
}

Enter fullscreen mode Exit fullscreen mode

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

. . . . . . . . . . . .
Terabox Video Player