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.
References