Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add a tutorial example for URL Shortner built using DiceDB and Go #1259

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 176 additions & 0 deletions docs/src/content/docs/tutorials/url-shortener.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
---
title: "Building a URL Shortener"
description: "Create a simple URL Shortener using DiceDB Go SDK."
---

This tutorial guides you through creating a URL shortener using DiceDB, a key-value store, with Go. We’ll set up endpoints to generate short URLs and redirect them to the original URLs.

Prerequisites

1. Go installed (at least version 1.18)
2. DiceDB server running locally

## Setup

1. Refer to [DiceDB Installation Guide](get-started/installation) to get your DiceDB server up and running with a simple Docker command.
2. Initialize a New Go Project
3. Install DiceDB Go SDK and other required packges.
```bash
go get github.com/dicedb/dicedb-go
go get github.com/gin-gonic/gin
go get github.com/google/uuid
```

## DiceDB Commands Used

Here are the main DiceDB commands we’ll use to store and retrieve URLs.

1. `Set` Command: Stores a key-value pair in DiceDB.
Syntax - `Set(key, value, expiration)`
`key`: Unique identifier (e.g., short URL code)
`value`: The data to store (serialized JSON)
`expiration`: Optional; 0 means no expiration

2. `Get` Command: Retrieves the value associated with a key.
Syntax - `Get(key)`
`key`: The identifier for the data to retrieve.

## Code overview

- `main.go`:
```go
package main

import (
"context"
"encoding/json"
"log"
"net/http"

"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/dicedb/dicedb-go" // DiceDB Go SDK
)

type URL struct {
ID string `json:"id"`
LongURL string `json:"long_url"`
ShortURL string `json:"short_url"`
}

var db *dicedb.Client

// Initialize DiceDB connection
func init() {
db = dicedb.NewClient(&dicedb.Options{
Addr: "localhost:7379",
})
}

// Creates a short URL from a given long URL
func CreateShortURL(c *gin.Context) {
var requestBody URL
if err := c.ShouldBindJSON(&requestBody); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
return
}

// Generate unique short ID and construct the short URL
shortID := uuid.New().String()[:8]
requestBody.ID = shortID
requestBody.ShortURL = "http://localhost:8080/" + shortID

// Serialize URL struct to JSON and store it in DiceDB
urlData, err := json.Marshal(requestBody)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save URL"})
return
}

if err := db.Set(context.Background(), shortID, urlData, 0).Err(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save URL"})
return
}

c.JSON(http.StatusCreated, gin.H{"short_url": requestBody.ShortURL})
}

// Redirects to the original URL based on the short URL ID
func RedirectURL(c *gin.Context) {
id := c.Param("id")

// Retrieve stored URL data from DiceDB
urlData, err := db.Get(context.Background(), id).Result()
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "URL not found"})
return
}

// Deserialize JSON data back into URL struct
var url URL
if err := json.Unmarshal([]byte(urlData), &url); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to decode URL data"})
return
}

// Redirect user to the original long URL
c.Redirect(http.StatusFound, url.LongURL)
}

func main() {
router := gin.Default()

// Define endpoints for creating short URLs and redirecting
router.POST("/shorten", CreateShortURL)
router.GET("/:id", RedirectURL)

// Start the server on port 8080
if err := router.Run(":8080"); err != nil {
log.Fatal("Failed to start server:", err)
}
}
```

## Explanation of Key Parts
1. Database Initialization: The `init()` function sets up a DiceDB client to connect to `localhost:7379`.
2. `CreateShortURL` Endpoint: Handles the `/shorten` route. It generates a unique ID, constructs the short URL, serializes the URL data, and saves it in DiceDB.
3. `RedirectURL` Endpoint: Handles the `/:id` route. It retrieves the original URL by the short ID from DiceDB and redirects the user to it.
4. Starting the Server: The `main` function starts the Gin server on port `8080`.

## Starting the application server

1. Start the application
```bash
go run main.go
```
This will start the application server on port 8080 by default, you should see output similar to
```bash
[GIN-debug] Listening and serving HTTP on :8080
```

## Interacting with the application

1. Start DiceDB: Ensure DiceDB is running.
2. Test the API:
- Shorten URL:
Send a POST request to `/shorten` with JSON body on Postman:
```
{
"long_url": "https://example.com"
}
```

OR

```curl
curl -X POST -H "Content-Type: application/json" -d '{"long_url": "https://example.com"}' http://localhost:8080/shorten
```

- Redirect to Original URL:
Send a GET request to `/:id` with the short URL ID on Postman

OR

```curl
curl -L http://localhost:8080/{short_id}
```
32 changes: 32 additions & 0 deletions examples/url-shortener/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Include any files or directories that you don't want to be copied to your
# container here (e.g., local build artifacts, temporary files, etc.).
#
# For more help, visit the .dockerignore file reference guide at
# https://docs.docker.com/go/build-context-dockerignore/

**/.DS_Store
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/bin
**/charts
**/docker-compose*
**/compose.y*ml
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
78 changes: 78 additions & 0 deletions examples/url-shortener/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# syntax=docker/dockerfile:1

# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Dockerfile reference guide at
# https://docs.docker.com/go/dockerfile-reference/

# Want to help us make this template better? Share your feedback here: https://forms.gle/ybq9Krt8jtBL3iCk7

################################################################################
# Create a stage for building the application.
ARG GO_VERSION=1.23.2
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION} AS build
WORKDIR /src

# Download dependencies as a separate step to take advantage of Docker's caching.
# Leverage a cache mount to /go/pkg/mod/ to speed up subsequent builds.
# Leverage bind mounts to go.sum and go.mod to avoid having to copy them into
# the container.
RUN --mount=type=cache,target=/go/pkg/mod/ \
--mount=type=bind,source=go.sum,target=go.sum \
--mount=type=bind,source=go.mod,target=go.mod \
go mod download -x

# This is the architecture you're building for, which is passed in by the builder.
# Placing it here allows the previous steps to be cached across architectures.
ARG TARGETARCH

# Build the application.
# Leverage a cache mount to /go/pkg/mod/ to speed up subsequent builds.
# Leverage a bind mount to the current directory to avoid having to copy the
# source code into the container.
RUN --mount=type=cache,target=/go/pkg/mod/ \
--mount=type=bind,target=. \
CGO_ENABLED=0 GOARCH=$TARGETARCH go build -o /bin/server .

################################################################################
# Create a new stage for running the application that contains the minimal
# runtime dependencies for the application. This often uses a different base
# image from the build stage where the necessary files are copied from the build
# stage.
#
# The example below uses the alpine image as the foundation for running the app.
# By specifying the "latest" tag, it will also use whatever happens to be the
# most recent version of that image when you build your Dockerfile. If
# reproducability is important, consider using a versioned tag
# (e.g., alpine:3.17.2) or SHA (e.g., alpine@sha256:c41ab5c992deb4fe7e5da09f67a8804a46bd0592bfdf0b1847dde0e0889d2bff).
FROM alpine:latest AS final

# Install any runtime dependencies that are needed to run your application.
# Leverage a cache mount to /var/cache/apk/ to speed up subsequent builds.
RUN --mount=type=cache,target=/var/cache/apk \
apk --update add \
ca-certificates \
tzdata \
&& \
update-ca-certificates

# Create a non-privileged user that the app will run under.
# See https://docs.docker.com/go/dockerfile-user-best-practices/
ARG UID=10001
RUN adduser \
--disabled-password \
--gecos "" \
--home "/nonexistent" \
--shell "/sbin/nologin" \
--no-create-home \
--uid "${UID}" \
appuser
USER appuser

# Copy the executable from the "build" stage.
COPY --from=build /bin/server /bin/

# Expose the port that the application listens on.
EXPOSE 8080

# What the container should run when it is started.
ENTRYPOINT [ "/bin/server" ]
17 changes: 17 additions & 0 deletions examples/url-shortener/compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
services:
dicedb:
image: dicedb/dicedb:latest
ports:
- "7379:7379"

url-shortener:
build:
context: .
dockerfile: Dockerfile
ports:
- "8000:8000"
depends_on:
- dicedb
environment:
- DICEDB_HOST=dicedb
- DICEDB_PORT=7379
41 changes: 41 additions & 0 deletions examples/url-shortener/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
module url-shortener

go 1.23.2

require github.com/gin-gonic/gin v1.10.0

require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
)

require (
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/dicedb/dicedb-go v0.0.0-20241026093718-570de4575be3
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/google/uuid v1.6.0
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading