A URL Shortener Service using Go, Iris and Bolt
Tags: go, iris, bolt, web
Published: 2019-05-28 20:53:18

---
title:  A URL Shortener Service using Go, Iris and Bolt
published: true
description: Wouldn’t be great to have your own shortened URLs inside your own domain?
tags: golang,iris,bolt,web
---


# A URL Shortener Service using Go, Iris and Bolt

If you follow the social media sites like Twitter and Google+, you’ll have noticed that rather than use the full URL we use a nice friendly shortened one like [t.co/HurCcP0nn9](https://t.co/HurCcP0nn9) and [bit.ly/iriscandothat1](http://bit.ly/iriscandothat1).

**Wouldn’t be great to have your own shortened URLs inside your own domain?**

Let’s review the basics first.

## The Tools

Programming Languages are just tools for us, but we need a safe, fast and “cross-platform” programming language to power our service.

[Go](https://golang.org/) is a [rapidly growing](http://www.tiobe.com/tiobe-index/) open source programming language designed for building simple, fast, and reliable software. Take a look [here](https://github.com/golang/go/wiki/GoUsers) which great companies use Go to power their services.

### Install the Go Programming Language

Extensive information about downloading & installing Go can be found [here](https://golang.org/dl/).

{% youtube 9x-pG3lvLi0 %}


> The article does not contain an introduction to the language itself, if you’re a newcomer I recommend you to bookmark this article, [learn ](https://github.com/golang/go/wiki/Learn)the language’s fundamentals and come back later on.

### The Dependencies

Many articles have been written, in the past, that lead users not to use a web framework because they’re “bad”. I have to tell you that there is no such a thing, it always depends on the (web) framework that you’re going to use.In production level, usually, we don’t have the time to code everything that we wanna use in the big company’s applications. In short terms: **good frameworks are helpful tools for any developer, company or startup and bad are waste of time, clear and simple**.

You’ll need only two required dependencies and one optional:

* [Iris ](https://github.com/kataras/iris), our web framework

* [Bolt (now bbolt)](https://github.com/etcd-io/bbolt), an embedded key/value database

* [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier) library, will help us to generate the short urls

Installing using go packages is a simple task, just open your terminal and execute the following commands:

```
$ go get github.com/kataras/iris/v12@latest
$ go get -u github.com/etcd-io/bbolt
$ go get -u github.com/satori/go.uuid
```

## The Best Part

Good, if we’re all in the same page, it’s time to learn how we can create a URL Shortener server that will be easy to deploy and can be extended even more!

To create a short link, we generate a random string and use Bolt to store the original URL with our random string as the key. When a “GET” request is issued to the shortened URL , we retrieve the original URL from Bolt. If such a value exists, we redirect, otherwise we respond with a corresponding message.

> For the sake of simplicity let’s say that the project is located at *$GOPATH/src/you/shortener *directory and the package name is *main*.

Our **front-end** is ridiculous simple, it contains just an index page and its “style”, templates are located to ./templates folder and the style at ./resources/css folder.

```html
<html>

<head>
    <meta charset="utf-8">
    <title>Golang URL Shortener</title>
    <link rel="stylesheet" href="/static/css/style.css" />
</head>

<body>
    <h2>Golang URL Shortener</h2>
    <h3>{{ .FORM_RESULT}}</h3>
    <form action="/shorten" method="POST">
        <input type="text" name="url" style="width: 35em;" />
        <input type="submit" value="Shorten!" />
    </form>
    {{ if IsPositive .URL_COUNT }}
        <p>{{ .URL_COUNT }} URLs shortened</p>
    {{ end }}

    <form action="/clear_cache" method="POST">
        <input type="submit" value="Clear DB" />
    </form>
</body>

</html>
```

```cs
body{
    background-color:silver;
}
```


Moving straight to the **database **level, we will create a simple implementation which will be able to **save **a shortened URL(key) and its original/full URL(value), **retrieve **a full URL based on the key and return the total number of all registered URLs to the database.

```go

package main

import (
	"bytes"

	"github.com/etcd-io/bbolt"
)

// Panic panics, change it if you don't want to panic on critical INITIALIZE-ONLY-ERRORS
var Panic = func(v interface{}) {
	panic(v)
}

// Store is the store interface for urls.
// Note: no Del functionality.
type Store interface {
	Set(key string, value string) error // error if something went wrong
	Get(key string) string              // empty value if not found
	Len() int                           // should return the number of all the records/tables/buckets
	Close()                             // release the store or ignore
}

var (
	tableURLs = []byte("urls")
)

// DB representation of a Store.
// Only one table/bucket which contains the urls, so it's not a fully Database,
// it works only with single bucket because that all we need.
type DB struct {
	db *bbolt.DB
}

var _ Store = &DB{}

// openDatabase open a new database connection
// and returns its instance.
func openDatabase(stumb string) *bbolt.DB {
	// Open the data(base) file in the current working directory.
	// It will be created if it doesn't exist.
	db, err := bbolt.Open(stumb, 0600, nil)
	if err != nil {
		Panic(err)
	}

	// create the buckets here
	var tables = [...][]byte{
		tableURLs,
	}

	db.Update(func(tx *bbolt.Tx) (err error) {
		for _, table := range tables {
			_, err = tx.CreateBucketIfNotExists(table)
			if err != nil {
				Panic(err)
			}
		}

		return
	})

	return db
}

// NewDB returns a new DB instance, its connection is opened.
// DB implements the Store.
func NewDB(stumb string) *DB {
	return &DB{
		db: openDatabase(stumb),
	}
}

// Set sets a shorten url and its key
// Note: Caller is responsible to generate a key.
func (d *DB) Set(key string, value string) error {
	return d.db.Update(func(tx *bbolt.Tx) error {
		b, err := tx.CreateBucketIfNotExists(tableURLs)
		// Generate ID for the url
		// Note: we could use that instead of a random string key
		// but we want to simulate a real-world url shortener
		// so we skip that.
		// id, _ := b.NextSequence()
		if err != nil {
			return err
		}

		k := []byte(key)
		valueB := []byte(value)
		c := b.Cursor()

		found := false
		for k, v := c.First(); k != nil; k, v = c.Next() {
			if bytes.Equal(valueB, v) {
				found = true
				break
			}
		}
		// if value already exists don't re-put it.
		if found {
			return nil
		}

		return b.Put(k, []byte(value))
	})
}

// Clear clears all the database entries for the table urls.
func (d *DB) Clear() error {
	return d.db.Update(func(tx *bbolt.Tx) error {
		return tx.DeleteBucket(tableURLs)
	})
}

// Get returns a url by its key.
//
// Returns an empty string if not found.
func (d *DB) Get(key string) (value string) {
	keyB := []byte(key)
	d.db.Update(func(tx *bbolt.Tx) error {
		b := tx.Bucket(tableURLs)
		if b == nil {
			return nil
		}
		c := b.Cursor()
		for k, v := c.First(); k != nil; k, v = c.Next() {
			if bytes.Equal(keyB, k) {
				value = string(v)
				break
			}
		}

		return nil
	})

	return
}

// GetByValue returns all keys for a specific (original) url value.
func (d *DB) GetByValue(value string) (keys []string) {
	valueB := []byte(value)
	d.db.Update(func(tx *bbolt.Tx) error {
		b := tx.Bucket(tableURLs)
		if b == nil {
			return nil
		}
		c := b.Cursor()
		// first for the bucket's table "urls"
		for k, v := c.First(); k != nil; k, v = c.Next() {
			if bytes.Equal(valueB, v) {
				keys = append(keys, string(k))
			}
		}

		return nil
	})

	return
}

// Len returns all the "shorted" urls length
func (d *DB) Len() (num int) {
	d.db.View(func(tx *bbolt.Tx) error {

		// Assume bucket exists and has keys
		b := tx.Bucket(tableURLs)
		if b == nil {
			return nil
		}

		b.ForEach(func([]byte, []byte) error {
			num++
			return nil
		})
		return nil
	})
	return
}

// Close shutdowns the data(base) connection.
func (d *DB) Close() {
	if err := d.db.Close(); err != nil {
		Panic(err)
	}
}
```


Let’s create our **factory** which will **generate** the shortened URLs for us!

```go
package main

import (
	"net/url"

	"github.com/satori/go.uuid"
)

// Generator the type to generate keys(short urls)
type Generator func() string

// DefaultGenerator is the defautl url generator
var DefaultGenerator = func() string {
	id, _ := uuid.NewV4()
	return id.String()
}

// Factory is responsible to generate keys(short urls)
type Factory struct {
	store     Store
	generator Generator
}

// NewFactory receives a generator and a store and returns a new url Factory.
func NewFactory(generator Generator, store Store) *Factory {
	return &Factory{
		store:     store,
		generator: generator,
	}
}

// Gen generates the key.
func (f *Factory) Gen(uri string) (key string, err error) {
	// we don't return the parsed url because #hash are converted to uri-compatible
	// and we don't want to encode/decode all the time, there is no need for that,
	// we save the url as the user expects if the uri validation passed.
	_, err = url.ParseRequestURI(uri)
	if err != nil {
		return "", err
	}

	key = f.generator()
	// Make sure that the key is unique
	for {
		if v := f.store.Get(key); v == "" {
			break
		}
		key = f.generator()
	}

	return key, nil
}
```

We should create a **main file** which will connect and combine all the components to run an http server which will serve our small URL Shortener service.

```go
package main

import (
	"html/template"

	"github.com/kataras/iris/v12"
)

func main() {
	// assign a variable to the DB so we can use its features later.
	db := NewDB("shortener.db")
	// Pass that db to our app, in order to be able to test the whole app with a different database later on.
	app := newApp(db)

	// release the "db" connection when server goes off.
	iris.RegisterOnInterrupt(db.Close)

	app.Run(iris.Addr(":8080"))
}

func newApp(db *DB) *iris.Application {
	app := iris.Default() // or app := iris.New()

	// create our factory, which is the manager for the object creation.
	// between our web app and the db.
	factory := NewFactory(DefaultGenerator, db)

	// serve the "./templates" directory's "*.html" files with the HTML std view engine.
	tmpl := iris.HTML("./templates", ".html").Reload(true)
	// register any template func(s) here.
	//
	// Look ./templates/index.html#L16
	tmpl.AddFunc("IsPositive", func(n int) bool {
		if n > 0 {
			return true
		}
		return false
	})

	app.RegisterView(tmpl)

	// Serve static files (css)
	app.StaticWeb("/static", "./resources")

	indexHandler := func(ctx iris.Context) {
		ctx.ViewData("URL_COUNT", db.Len())
		ctx.View("index.html")
	}
	app.Get("/", indexHandler)

	// find and execute a short url by its key
	// used on http://localhost:8080/u/dsaoj41u321dsa
	execShortURL := func(ctx iris.Context, key string) {
		if key == "" {
			ctx.StatusCode(iris.StatusBadRequest)
			return
		}

		value := db.Get(key)
		if value == "" {
			ctx.StatusCode(iris.StatusNotFound)
			ctx.Writef("Short URL for key: '%s' not found", key)
			return
		}

		ctx.Redirect(value, iris.StatusTemporaryRedirect)
	}
	app.Get("/u/{shortkey}", func(ctx iris.Context) {
		execShortURL(ctx, ctx.Params().Get("shortkey"))
	})

	app.Post("/shorten", func(ctx iris.Context) {
		formValue := ctx.FormValue("url")
		if formValue == "" {
			ctx.ViewData("FORM_RESULT", "You need to a enter a URL")
			ctx.StatusCode(iris.StatusLengthRequired)
		} else {
			key, err := factory.Gen(formValue)
			if err != nil {
				ctx.ViewData("FORM_RESULT", "Invalid URL")
				ctx.StatusCode(iris.StatusBadRequest)
			} else {
				if err = db.Set(key, formValue); err != nil {
					ctx.ViewData("FORM_RESULT", "Internal error while saving the URL")
					app.Logger().Infof("while saving URL: " + err.Error())
					ctx.StatusCode(iris.StatusInternalServerError)
				} else {
					ctx.StatusCode(iris.StatusOK)
					shortenURL := "http://" + app.ConfigurationReadOnly().GetVHost() + "/u/" + key
					ctx.ViewData("FORM_RESULT",
						template.HTML("<pre><a target='_new' href='"+shortenURL+"'>"+shortenURL+" </a></pre>"))
				}

			}
		}

		indexHandler(ctx) // no redirect, we need the FORM_RESULT.
	})

	app.Post("/clear_cache", func(ctx iris.Context) {
		db.Clear()
		ctx.Redirect("/")
	})

	return app
}
```

Finally… let’s build and run our application!

```go
$ cd $GOPATH/src/github.com/myname/url-shortener
$ go build # build 
$ ./url-shortener # run
```


## **BONUS** section

**Testing** our apps is always a useful thing to do, therefore I’ll give you the first idea on how you can test these type of web applications with Iris, straight to the code below.

```go
package main

import (
	"io/ioutil"
	"os"
	"testing"
	"time"

	"github.com/kataras/iris/v12/httptest"
)

// TestURLShortener tests the simple tasks of our url shortener application.
// Note that it's a pure test.
// The rest possible checks is up to you, take it as as an exercise!
func TestURLShortener(t *testing.T) {
	// temp db file
	f, err := ioutil.TempFile("", "shortener")
	if err != nil {
		t.Fatalf("creating temp file for database failed: %v", err)
	}

	db := NewDB(f.Name())
	app := newApp(db)

	e := httptest.New(t, app)
	originalURL := "https://google.com"

	// save
	e.POST("/shorten").
		WithFormField("url", originalURL).Expect().
		Status(httptest.StatusOK).Body().Contains("<pre><a target='_new' href=")

	keys := db.GetByValue(originalURL)
	if got := len(keys); got != 1 {
		t.Fatalf("expected to have 1 key but saved %d short urls", got)
	}

	// get
	e.GET("/u/" + keys[0]).Expect().
		Status(httptest.StatusTemporaryRedirect).Header("Location").Equal(originalURL)

	// save the same again, it should add a new key
	e.POST("/shorten").
		WithFormField("url", originalURL).Expect().
		Status(httptest.StatusOK).Body().Contains("<pre><a target='_new' href=")

	keys2 := db.GetByValue(originalURL)
	if got := len(keys2); got != 1 {
		t.Fatalf("expected to have 1 keys even if we save the same original url but saved %d short urls", got)
	} // the key is the same, so only the first one matters.

	if keys[0] != keys2[0] {
		t.Fatalf("expected keys to be equal if the original url is the same, but got %s = %s ", keys[0], keys2[0])
	}

	// clear db
	e.POST("/clear_cache").Expect().Status(httptest.StatusOK)
	if got := db.Len(); got != 0 {
		t.Fatalf("expected database to have 0 registered objects after /clear_cache but has %d", got)
	}

	// give it some time to release the db connection
	db.Close()
	time.Sleep(1 * time.Second)
	// close the file
	if err := f.Close(); err != nil {
		t.Fatalf("unable to close the file: %s", f.Name())
	}

	// and remove the file
	if err := os.Remove(f.Name()); err != nil {
		t.Fatalf("unable to remove the file from %s", f.Name())
	}

	time.Sleep(1 * time.Second)

}
```

Running the test

```
$ cd $GOPATH/src/github.com/myname/url-shortener
$ go test -v # test
```


## That’s all, pretty simple. Right?

Share your thoughts about this post and let me know what awesome apps you’re planing to build with Go + Iris!

**Thanks for taking the time to read the whole article, I admire you for your patience :)**

> The full source code is located [here](https://github.com/kataras/iris/tree/v12/_examples/tutorial/url-shortener).
If you have any further questions please feel free to leave a comment below or ask [here](https://chat.iris-go.com)!

## Thank you, once again

Don't forget to check out my medium profile and twitter as well, I'm posting some  stuff there too:)

- https://medium.com/@kataras 
- https://twitter.com/MakisMaropoulos