visit
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, .
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.
mkdir project-name
cd project-name
go mod init project-name
go get github.com/labstack/echo/v4
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 /
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.
Create a new struct called Link
and a map called LinkMap
:
type Link struct {
Id string
Url string
}
var linkMap = map[string]*models.Link{}
var linkMap = map[string]*Link{ "example": { Id: "example", Url: "//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.
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: "//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)
}
go run .
go mod tidy
If you head to localhost:8080/example
, you should be redirected to example.com
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
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 = "//" + 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.
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: "//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 = "//" + url
}
id := generateRandomString(8)
linkMap[id] = &Link{Id: id, Url: url}
return c.Redirect(http.StatusSeeOther, "/")
}