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
- Go installation
- Knowledge of Gin and error handling
- Redis installation
- Code editor , VSCode is my choice
- 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.
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)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.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)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

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
GetMovie: Returns a movie or an error if anyGetMovies: Returns all movies or an error if anyCreateMovie: Adds a new movie and returns the new movie an error if anyUpdateMovie: Updates an existing move and returns the updated movieDeleteMovie: 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

Get Movie

Get Movies

Update Movie

Delete Movie

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.


