Building a CRUD REST API using Redis DB in GO


GO

Reviewer: Deepak Prasad

Introduction to CRUD REST API Framework

Go or Golang is an open source programming language developed and supported by Google. It is mostly used to build backend applications where it interacts with different types of databases like SQL and NoSQL databases. Redis on the other hand is an open source in memory data store that can be used as a database, streaming engine and as a message broker. In this article , we are going to learn how to use these two technologies, Go and Redis to develop a simple CRUD application. We will use redis as an in memory database.

 

Prerequisite

  1. Go installation
  2. Knowledge of Gin and error handling
  3. Redis installation
  4. Code editor , VSCode is my choice
  5. Postman application

 

Understanding Redis DB

Most companies use redis as an in-memory data structure store. It provides data structures such as strings, hashes, lists, sets , sorted sets. In this article , we are going to use strings to store our data. To learn more about these other data structures in redis.

Redis provides operations to help you interact with data easily. Unlike other database types that require you to query data in the database using SQL syntax, redis provides easy to use commands for interacting with data.It has over 50 commands that you can use to interact with it.

Considering that we are building a CRUD application, we are going to use the following commands.

  1. HGET: Returns the value associated with the a field, or nil if is not present in the hash . Has a time complexity of O(1)
  2. HSET: Sets a field in the hash store at key to value. If a key does not exist, a new key holding the hash is created, if a field already exists in the hash, it is overwritten. It returns the number of fields that were added.Takes O(1) for each key/field added.
  3. HGETALL: Returns all the fields and values of the hash store at key. In the returned value, every field name is followed by its value, so the length of the reply is twice the size of the hash. Takes O(n)
  4. HDel: Removes the specified fields from the hash store at key.Specified fields that do not exist within the hash are ignored.If key does not exist, it is treated as an empty hash and this command returns 0.Takes O(n)

 

Application structure

This application will make use of a very simple structure to get us app and running. For small applications , this structure can be used in production. The application structure will have one module called cache and main.go file at the root. Using the terminal, navigate to your working directory and issue the below commands.

Create a working directory and navigate into it

mkdir go-crud-redis && cd go-crud-redis

Create main.go file

touch main.go

Create redis cache directory and add redis.go file in it

mkdir cache && cd cache && touch redis.go

Initialize go module

go mod init example.com/go-crud-redis

Install dependencies

go get -u github.com/gin-gonic/gin
go get -u github.com/go-redis/redis/v7
go get -u github.com/google/uuid

Building a CRUD REST API using Redis DB in GO

 

Configure Redis Data Store

cache/redis.go

package cache
 
import (
   "encoding/json"
   "errors"
   "time"
 
   "github.com/go-redis/redis/v7"
   "github.com/google/uuid"
)
 
type Movie struct {
   Id          string `json:"id"`
   Title       string `json:"title"`
   Description string `json:"description"`
}
 
type MovieService interface {
   GetMovie(id string) (*Movie, error)
   GetMovies() ([]*Movie, error)
   CreateMovie(movie *Movie) (*Movie, error)
   UpdateMovie(movie *Movie) (*Movie, error)
   DeleteMovie(id string) error
}

Movie Model and MovieService

We will start off by adding code for our database model called Movie, and a movie service called MovieService. The Movie model will have three attributes , namely id, title, and description. These are the attributes that all our movies will have. On the other hand the MovieService will contain all the operations that will be performed on each Movie model while interacting with redis at the same time.These operations are

  1. GetMovie : Returns a movie or an error if any
  2. GetMovies: Returns all movies or an error if any
  3. CreateMovie: Adds a new movie and returns the new movie an error if any
  4. UpdateMovie: Updates an existing move and returns the updated movie
  5. DeleteMovie: Deletes a movie, and returns nil if delete is success else returns an error

 

Configure Redis client

After adding the Movie model and MovieService interface, add the below code to create a redis client that implements the MovieService.

type redisCache struct {
   host string
   db   int
   exp  time.Duration
}
 
func NewRedisCache(host string, db int, exp time.Duration) MovieService {
   return &redisCache{
       host: host,
       db:   db,
       exp:  exp,
   }
}
 
func (cache redisCache) getClient() *redis.Client {
   return redis.NewClient(&redis.Options{
       Addr:     cache.host,
       Password: "",
       DB:       cache.db,
   })
}

In this section we start by defining a redis cache struct. A redis client is required to have the host, database and expiration duration. Next we create a redis contractor function that returns a new redis client. The NewRedisCache function takes in as arguments, the host, database and expiration duration and returns a MovieService service. In order to return a MoveService, we need to implement all the methods in the MoveService interface. We will add this later.Lastly we create a getClient() method that creates a new redis client.

 

Implementing MoveService methods

func (cache redisCache) CreateMovie(movie *Movie) (*Movie, error) {
   c := cache.getClient()
   movie.Id = uuid.New().String()
   json, err := json.Marshal(movie)
   if err != nil {
       return nil, err
   }
   c.HSet("movies", movie.Id, json)
   if err != nil {
       return nil, err
   }
   return movie, nil
}
 
func (cache redisCache) GetMovie(id string) (*Movie, error) {
   c := cache.getClient()
   val, err := c.HGet("movies", id).Result()
 
   if err != nil {
       return nil, err
   }
   movie := &Movie{}
   err = json.Unmarshal([]byte(val), movie)
 
   if err != nil {
       return nil, err
   }
   return movie, nil
}
 
func (cache redisCache) GetMovies() ([]*Movie, error) {
   c := cache.getClient()
   movies := []*Movie{}
   val, err := c.HGetAll("movies").Result()
   if err != nil {
       return nil, err
   }
   for _, item := range val {
       movie := &Movie{}
       err := json.Unmarshal([]byte(item), movie)
       if err != nil {
           return nil, err
       }
       movies = append(movies, movie)
   }
 
   return movies, nil
}
 
func (cache redisCache) UpdateMovie(movie *Movie) (*Movie, error) {
   c := cache.getClient()
   json, err := json.Marshal(&movie)
   if err != nil {
       return nil, err
   }
   c.HSet("movies", movie.Id, json)
   if err != nil {
       return nil, err
   }
   return movie, nil
}
func (cache redisCache) DeleteMovie(id string) error {
   c := cache.getClient()
   numDeleted, err := c.HDel("movies", id).Result()
   if numDeleted == 0 {
       return errors.New("movie to delete not found")
   }
   if err != nil {
       return err
   }
   return nil
}

CreateMovie

CreateMovie method, takes movie data and returns a pointer or an error. We start by creating a redis client using the c := cache.getClient() statement. Next we create a unique id for the movie. Since we are working with strings, we use json.Marshal(movie) to convert our movie of type Movie to a string. If there is no error after marshaling, we store the new movie into the database using c.HSet(“movies”, movie.Id, json) statement. Please note that the first argument for HSet() is the hash and the movie.Id is the key and json is the field.If all steps are successful , we return the created movie to the client, hence return an error.

GetMovie

GetMovie method takes the id of the movie to return and returns the movie to the client if present else returns an error. After creating a client, we use val, err := c.HGet(“movies”, id).Result(). If the HGet operation is successful, we create a movie type that will be populated with data. Next we unmarshal the value from HGet using err := json.Unmarshal([]byte(val), movie) . After a successful get operation, we return the movie to the client.

GetMovies

GetMovies method returns all the movies from the movies hash. We start by creating a redis client and then declaring a movies array using the movies := []*Movie{} statement. To get all the movies from the movies hash, we use val, err := c.HGetAll(“movies”).Result() statement. We then range over the values in the val variable and append each item in the movies array. Then we return all the movies to the client

UpdateMovie

UpdateMovie method takes the data to update a movie as an argument and return the updated movie to the client. We marshal the data using json, err := json.Marshal(&movie). We then use c.HSet(“movies”, movie.id, json) to update the data. This statement is the same as the one for creating a new movie, but the catch is the HSet() command sets a field in the hash store at key to value. If a key does not exist, a new key holding the hash is created, if a field already exists in the hash, it is overwritten.

DeleteMovie

DeleteMovie method takes an id of the move to delete , deletes the movie if found, else returns an error.To delete a move we use the numDeleted , err := c.HDel(“movies”, id).Result() command, If the movie is present in the hash and deleted successful, the numDeleted variable returns 1, in numDeleted variable is 0, it means the move is not found. DeleteMovie marks the end of our code in cache/redis.go file.

 

Create Server for HTTP Request

Next we are going to write code in the main.go file to handle HTTP requests using Gin web framework. Navigate to the main.go file and add the following code

main.go

package main
 
import (
   "fmt"
   "net/http"
 
   "example.com/go-crud-redis/cache"
 
   "github.com/gin-gonic/gin"
)
 
var (
   redisCache = cache.NewRedisCache("localhost:6379", 0, 1)
)
func main() {
   r := gin.Default()
 
   r.POST("/movies", func(ctx *gin.Context) {
       var movie cache.Movie
       if err := ctx.ShouldBind(&movie); err != nil {
           ctx.JSON(http.StatusBadRequest, gin.H{
               "error": err.Error(),
           })
           return
       }
       res, err := redisCache.CreateMovie(&movie)
       if err != nil {
           ctx.JSON(http.StatusBadRequest, gin.H{
               "error": err.Error(),
           })
           return
       }
       ctx.JSON(http.StatusOK, gin.H{
           "movie": res,
       })
 
   })
   r.GET("/movies", func(ctx *gin.Context) {
       movies, err := redisCache.GetMovies()
       if err != nil {
           ctx.JSON(http.StatusBadRequest, gin.H{
               "error": err.Error(),
           })
           return
       }
       ctx.JSON(http.StatusOK, gin.H{
           "movies": movies,
       })
   })
   r.GET("/movies/:id", func(ctx *gin.Context) {
       id := ctx.Param("id")
       movie, err := redisCache.GetMovie(id)
       if err != nil {
           ctx.JSON(http.StatusNotFound, gin.H{
               "message": "movie not found",
           })
           return
       }
       ctx.JSON(http.StatusOK, gin.H{
           "movie": movie,
       })
   })
   r.PUT("/movies/:id", func(ctx *gin.Context) {
       id := ctx.Param("id")
       res, err := redisCache.GetMovie(id)
       if err != nil {
           ctx.JSON(http.StatusBadRequest, gin.H{
               "error": err.Error(),
           })
           return
       }
 
       var movie cache.Movie
 
       if err := ctx.ShouldBind(&movie); err != nil {
           ctx.JSON(http.StatusBadRequest, gin.H{
               "error": err.Error(),
           })
           return
       }
 
       res.Title = movie.Title
       res.Description = movie.Description
       res, err = redisCache.UpdateMovie(res)
 
       if err != nil {
           ctx.JSON(http.StatusBadRequest, gin.H{
               "error": err.Error(),
           })
           return
       }
       ctx.JSON(http.StatusOK, gin.H{
           "movie": res,
       })
   })
   r.DELETE("/movies/:id", func(ctx *gin.Context) {
       id := ctx.Param("id")
       err := redisCache.DeleteMovie(id)
       if err != nil {
           ctx.JSON(http.StatusNotFound, gin.H{
               "error": err.Error(),
           })
           return
       }
       ctx.JSON(http.StatusOK, gin.H{
           "message": "movie deleted successfully",
       })
   })
   fmt.Println(r.Run(":5000"))
 
}

In the main function,we route HTTP CRUD methods with their respective handlers. To run the application, in the root directory of your application issue the below command.

Example

$ go run main.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
 
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env:   export GIN_MODE=release
- using code:  gin.SetMode(gin.ReleaseMode)
 
[GIN-debug] POST   /movies                   --> main.main.func1 (3 handlers)
[GIN-debug] GET    /movies                   --> main.main.func2 (3 handlers)
[GIN-debug] GET    /movies/:id               --> main.main.func3 (3 handlers)
[GIN-debug] PUT    /movies/:id               --> main.main.func4 (3 handlers)
[GIN-debug] DELETE /movies/:id               --> main.main.func5 (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on :5000

 

Testing the application

Create Movie

Building a CRUD REST API using Redis DB in GO


Get Movie

Building a CRUD REST API using Redis DB in GO


Get Movies

Building a CRUD REST API using Redis DB in GO


Update Movie

Building a CRUD REST API using Redis DB in GO


Delete Movie

Building a CRUD REST API using Redis DB in GO

 

Summary

In this article we learn how to get up and running with Go and redis. We learn how to create a simple CRUD application using Go as the run time, Gin framework for HTTP request handling and Redis for storing data. Redis is a good option for applications that are latency critical because retrieving data from memory is fast compared to retrieving from the hard drive. Apart from storing data, redis can also be used for data streaming  and as a message broker.

 

References

https://redis.io/commands

 

Antony Shikubu

Antony Shikubu

He is highly skilled software developer with expertise in Python, Golang, and AWS cloud services. Skilled in building scalable solutions, he specializes in Django, Flask, Pandas, and NumPy for web apps and data processing, ensuring robust and maintainable code for diverse projects. You can reach out to him on his LinkedIn profile.

Can't find what you're searching for? Let us assist you.

Enter your query below, and we'll provide instant results tailored to your needs.

If my articles on GoLinuxCloud has helped you, kindly consider buying me a coffee as a token of appreciation.

Buy GoLinuxCloud a Coffee

For any other feedbacks or questions you can send mail to admin@golinuxcloud.com

Thank You for your support!!

Leave a Comment