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.
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.
In this project, I will be using echo as the HTTP server and the standard HTML library.
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.
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 /
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.
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
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.
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, "/")
}
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:
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