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 Go installed on your system. 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: 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 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 . 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. www.somelongwebsite.com/articles/this-is-a-super-long-link bit.ly/abc123 Requirements Go installed on your system. A code editor, eg vs code, neovim Go installed on your system. A code editor, eg vs code , neovim vs code neovim In this project, I will be using echo as the HTTP server and the standard HTML library. echo Project Setup Create a new directory to house our project mkdir project-name cd project-name mkdir project-name cd project-name Assuming you have Golang installed. Create a new go module (project): go mod init project-name go mod init project-name Before we start writing any code, we first have to install echo: go get github.com/labstack/echo/v4 go get github.com/labstack/echo/v4 Now, create a new file called main.go touch 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")) } 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. /:id 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 / /submit / 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 /M61YlA 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) } 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. length 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 : Link LinkMap type Link struct { Id string Url string } var linkMap = map[string]*models.Link{} 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", }, } 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. RedirectHandler 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 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. /123 LinkMap 301 Permanently Moved 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) } 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 . go run . You might also want to install any missing dependencies: go mod tidy go mod tidy If you head to localhost:8080/example , you should be redirected to example.com localhost:8080/example Submission Handlers We will now define two new routes inside of our main function e.GET("/", IndexHandler) e.POST("/submit", SubmitHandler) 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: IndexHandler 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 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 / 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 templ e. templ The submission handler SubmitHandler should look something like this SubmitHandler 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, "/") } 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, "/") } 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: Enhance the input validation Track link clicks + Statistics Page Improve UI (HTML) Dockerizing the application ++ 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 app.4rkal.com here Join My Mailing List Subscribe here: https://newsletter.4rkal.com/subscription/form https://newsletter.4rkal.com/subscription/form