Building a URL shortener in Go with Nitric

What we'll be doing

  1. Use Nitric to create a simple URL shortener service.
  2. Expose an HTTP endpoint for shortening URLs.
  3. Store shortened URLs in a Key-Value (KV) store.
  4. Redirect users to the original URL when accessing the short code.
  5. Run locally for testing.
  6. Deploy to AWS.

Prerequisites

Getting started

We'll start by creating a new project for our WebSocket application. The finished source can be found here.

nitric new url-shortener go-starter

Next, open the project in your preferred editor:

cd url-shortener

Make sure all dependencies are resolved:

go mod tidy

The scaffolded project should have the following structure:

+--services/
| +-- hello/
| +-- main.go
+--nitric.yaml
+--go.mod
+--go.sum
+--golang.dockerfile
+--.gitignore
+--README.md

You can test the project to ensure everything is working as expected:

nitric start

If everything is working as expected, you can now delete all files/folders in the services/ folder. We'll create new services in this guide.

Resource Initialization

We'll start by creating a resources package that defines and initializes the API and KV store from the Nitric SDK. At this point, we won't request any permissions like get or set for the KV store. We'll do that in the individual services to ensure least privilege provisioning.

package resources
import (
"sync"
"github.com/nitrictech/go-sdk/nitric"
"github.com/nitrictech/go-sdk/nitric/apis"
"github.com/nitrictech/go-sdk/nitric/keyvalue"
)
type Resource struct {
MainApi apis.Api
UrlKvStore keyvalue.KvStore
}
var (
resource *Resource
resourceOnce sync.Once
)
func Get() *Resource {
resourceOnce.Do(func() {
mainApi := nitric.NewApi("main")
resource = &Resource{
MainApi: mainApi,
UrlKvStore: nitric.NewKv("urls"),
}
})
return resource
}

Building the URL Shortener Application

Create a new folder called shortener within the services directory. Inside this folder, add a file named main.go, and include the following code:

package main
import (
"math/rand"
"github.com/nitrictech/go-sdk/nitric"
"github.com/nitrictech/go-sdk/nitric/apis"
"github.com/nitrictech/go-sdk/nitric/keyvalue"
"github.com/nitrictech/templates/go-starter/resources"
)
func main() {
// Initialize the resources defined in resources.go
urlKvStore := resources.Get().UrlKvStore.Allow(keyvalue.KvStoreSet, keyvalue.KvStoreGet)
// POST /shorten - Shorten a given URL
resources.Get().MainApi.Post("/shorten", func(ctx *apis.Ctx) {
// TODO: implement app logic
})
// GET /:code - Redirect to the original URL associated with the short code
resources.Get().MainApi.Get("/:code", func(ctx *apis.Ctx) {
// TODO: implement app logic
})
nitric.Run()
}

Here we're creating placeholders for our API routes and initializing our KV store with permissions so we can GET and SET values.

Next, let's add a helper function to handle URL shortening and an inline struct to hold our URL data.

func generateShortCode() string {
s := ""
for i := 0; i < 6; i++ {
s += string(rand.Intn(26) + 97) // Generate a lowercase letter
}
return s
}
var shortenData struct {
Url string `json:"url"`
}

Let's implement our shorten route:

// POST /shorten - Shorten a given URL
resources.Get().MainApi.Post("/shorten", func(ctx *apis.Ctx) {
err := json.Unmarshal(ctx.Request.Data(), &shortenData)
if err != nil || strings.TrimSpace(shortenData.Url) == "" {
ctx.Response.Status = http.StatusBadRequest
ctx.Response.Body = []byte("Invalid or missing URL")
return
}
shortCode := generateShortCode()
// Store the mapping of short code -> original URL
err = urlKvStore.Set(context.Background(), shortCode, map[string]interface{}{
"url": shortenData.Url,
})
if err != nil {
ctx.Response.Status = 500
ctx.Response.Body = []byte("Error shortening URL")
return
}
// Extract the origin from headers (for demonstration), then return the short URL
origin := ""
if val, ok := ctx.Request.Headers()["X-Forwarded-For"]; ok {
origin = val[0]
} else if val, ok := ctx.Request.Headers()["x-forwarded-for"]; ok {
origin = val[0]
}
ctx.Response.Body = []byte(fmt.Sprintf("%s/%s", origin, shortCode))
})

Finally, we'll add a redirect handler to send users to the long URL when they use the short code:

// GET /:code - Redirect to the original URL associated with the short code
resources.Get().MainApi.Get("/:code", func(ctx *apis.Ctx) {
code := ctx.Request.PathParams()["code"]
data, err := urlKvStore.Get(context.Background(), code)
if err == nil {
ctx.Response.Headers["Location"] = []string{data["url"].(string)}
ctx.Response.Status = 301
} else {
fmt.Println("Error getting URL: ", err)
ctx.Response.Status = 404
}
})

Running locally

Ensure all dependencies are resolved and start the project:

go mod tidy
nitric start

Nitric will run the service locally. You can test the endpoints using tools like curl or Postman. For example, to shorten a URL:

curl -X POST localhost:4001/shorten -d '{"url":"https://example.com"}' -H "Content-Type: application/json"

You should receive a response containing the short code which you can use in a browser to test the redirect:

http://localhost:4001/{shortcode}

Deploy to the Cloud

When you're ready to deploy this to the cloud, Nitric makes it straightforward. In this example, we’ll deploy to AWS. First, set up your credentials and configuration for the nitric/aws provider.

Create a Stack

Create a dev stack that uses the aws provider:

nitric stack new dev aws

This command creates a nitric.dev.yaml file defining your deployment target. Edit it to configure your preferred AWS region (e.g., us-east-1).

Deploying

Deploy the stack to the cloud:

nitric up

Once deployment is complete, you’ll have live endpoints accessible via AWS infrastructure. Test the endpoints using the new domain provided after deployment.

To remove deployed resources:

nitric down

Summary

In this guide, we’ve built a simple URL shortening service using Go and Nitric. We’ve shown how to:

  • Set up an API to shorten URLs and redirect users.
  • Use a Key-Value store to persist short code mappings.
  • Run and test the application locally before deploying it to AWS.
Last updated on Dec 24, 2024