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
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. 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)
}
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 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)
}
Run the server
go run .
You might also want to install any missing dependencies:
go mod tidy
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)
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)
}
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, "/")
}
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, "/")
}
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:
- Enhance the input validation
- Track link clicks + Statistics Page
- Improve UI (html)
- Dockerizing the application
- ++
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
Top comments (0)