Storage Layer 📦
Tags: go, redis, database, coding
Published: 2022-10-20 13:15:26

In this part, we will be working on building **the storage layer** of our short URL application, so let's start!

📝 If **Redis** is not yet installed on your computer, you can do it following [the instructions for installation](https://redis.io/docs/getting-started/installation/) with respect to your operating system.

First thing first, we will install [Redis client for Golang](https://github.com/go-redis/redis)

```
go get github.com/go-redis/redis/v9
```

then we create the **store** folder in the project, and two empty Go files: **store.go** and **store_test.go**

```
├── go.mod
├── go.sum
├── main.go
└── shortener
   ├── shortener.go
   └── shortener_test.go
└── store
   ├── store.go
   └── store_test.go
```

Now we can define wrapper structs and initialize the store service, in this case our Redis client. 

```go
package store

import (
	"context"
	"fmt"
	"github.com/go-redis/redis/v9"
	"time"
)

// StorageService is struct wrapper around raw Redis client
type StorageService struct {
	redisClient *redis.Client
}

// Top level declarations for the storeService and Redis context
var (
	storeService = &StorageService{}
	ctx          = context.Background()
)

const CacheDuration = 6 * time.Hour

// InitializeStore is initializing the store service and return a store pointer
func InitializeStore() *StorageService {
	rdb := redis.NewClient(&redis.Options{
		Addr:     "localhost:6379",
		Password: "", // no password set
		DB:       0,  // use default DB
	})

	pong, err := rdb.Ping(ctx).Result()
	if err != nil {
		panic(fmt.Sprintf("Error init Redis: %v", err))
	}

	fmt.Printf("\nRedis started successfully: pong message = {%s}", pong)
	storeService.redisClient = rdb
	return storeService
}
```

Now that our data store service has successfully been initialized, it's time to consider what **storage API** to offer for our shortener server.

1.	We want to be able to save the mapping between the original URL and the generated short URL.
2.	We should be able to retrieve the initial long URL once the short is provided.

As the next step let's update **store.go** and implement our storage API.

```go
func SaveURLInRedis(shortURL, originalURL string) {
	err := storeService.redisClient.Set(ctx, shortURL, originalURL, CacheDuration).Err()
	if err != nil {
		panic(fmt.Sprintf("Failed SaveURLInRedis | Error: %v - shortURL: %s - originalURL: %s\n", 
			err, shortURL, originalURL))
	}
}

func RetrieveInitialURLFromRedis(shortURL string) string {
	result, err := storeService.redisClient.Get(ctx, shortURL).Result()
	if err != nil {
		panic(fmt.Sprintf("Failed RetrieveInitialURLFromRedis | Error: %v - shortURL: %s\n", 
			err, shortURL))
	}
	return result
}
```

It was fairly straightforward. Great job!

Since we already created **store_test.go** file, we will set up the test shell first and then write unit test for the storage APIs.

```go
package store

import (
	"github.com/stretchr/testify/assert"
	"testing"
)

var testStoreService = &StorageService{}

func init() {
	testStoreService = InitializeStore()
}

func TestStoreInit(t *testing.T) {
	assert.True(t, testStoreService.redisClient != nil)
}

func TestInsertionAndRetrieval(t *testing.T) {
	initialLink := "https://go.dev/doc/tutorial/getting-started"
	shortURL := "dysg5Fas"

	// Persist data mapping
	SaveURLInRedis(shortURL, initialLink)

	// Retrieve initial URL
	retrievedUrl := RetrieveInitialURLFromRedis(shortURL)
	assert.Equal(t, initialLink, retrievedUrl)
}
```

📝 Don't forget to run `go test ./...` to check if all your tests work as expected (✅).

As our storage service is set up, we will expose a Rest API endpoint for encoding and decoding the URLs in the next part.

---

Originally published at [projectex.dev](https://projectex.dev/challenges/how-to-build-a-url-shortener-in-go-with-redis/)