Build a Gin‑Based CRUD API with MongoDB and Redis Caching in Go
This guide demonstrates how to create a simple Go web API using the Gin framework, define a recipe data model, implement CRUD endpoints backed by MongoDB, add Redis caching for list queries, and provide a complete runnable example with testing scripts.
Introduction
This article shows how to build a Web API with the Gin framework in Go, providing basic CRUD functionality, storing data in MongoDB, and using Redis as a cache layer.
Define Data Model
package models
import (
"time"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type Recipe struct {
ID primitive.ObjectID `json:"id" bson:"_id"`
Name string `json:"name" bson:"name"`
Tags []string `json:"tags" bson:"tags"`
Ingredients []string `json:"ingredients" bson:"ingredients"`
Instructions []string `json:"instructions" bson:"instructions"`
PublishedAt time.Time `json:"publishedAt" bson:"publishedAt"`
}Design API
Endpoints:
GET /recipes – return a list of recipes
POST /recipes – create a new recipe
PUT /recipes/{id} – update an existing recipe
DELETE /recipes/{id} – delete a recipe
GET /recipes/search?tag=X – query recipes by tag
Implement Handlers
package handlers
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"buildginapp/models"
)
type RecipesHandler struct {
collection *mongo.Collection
ctx context.Context
redisClient *redis.Client
}
func NewRecipesHandler(ctx context.Context, collection *mongo.Collection, redisClient *redis.Client) *RecipesHandler {
return &RecipesHandler{collection: collection, ctx: ctx, redisClient: redisClient}
}
// GET /recipes
func (handler *RecipesHandler) ListRecipesHandler(c *gin.Context) {
val, err := handler.redisClient.Get("recipes").Result()
if err == redis.Nil {
log.Println("Request to MongoDB")
cur, err := handler.collection.Find(handler.ctx, bson.M{})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer cur.Close(handler.ctx)
var recipes []models.Recipe
for cur.Next(handler.ctx) {
var recipe models.Recipe
cur.Decode(&recipe)
recipes = append(recipes, recipe)
}
data, _ := json.Marshal(recipes)
handler.redisClient.Set("recipes", string(data), 3600*time.Second)
c.JSON(http.StatusOK, recipes)
} else if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
} else {
log.Println("Request to redis")
var recipes []models.Recipe
json.Unmarshal([]byte(val), &recipes)
c.JSON(http.StatusOK, recipes)
}
}
// POST /recipes
func (handler *RecipesHandler) NewRecipeHandler(c *gin.Context) {
var recipe models.Recipe
if err := c.ShouldBindJSON(&recipe); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
recipe.ID = primitive.NewObjectID()
recipe.PublishedAt = time.Now()
_, err := handler.collection.InsertOne(handler.ctx, recipe)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error while inserting a new recipe"})
return
}
handler.redisClient.Del("recipes")
c.JSON(http.StatusOK, recipe)
}
// PUT /recipes/:id
func (handler *RecipesHandler) UpdateRecipeHandler(c *gin.Context) {
id := c.Param("id")
var recipe models.Recipe
if err := c.ShouldBindJSON(&recipe); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
objectId, _ := primitive.ObjectIDFromHex(id)
_, err := handler.collection.UpdateOne(handler.ctx, bson.M{"_id": objectId}, bson.D{{"$set", bson.D{{"name", recipe.Name}, {"instructions", recipe.Instructions}, {"ingredients", recipe.Ingredients}, {"tags", recipe.Tags}}}})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "update success"})
}
// DELETE /recipes/:id
func (handler *RecipesHandler) DeleteRecipeHandler(c *gin.Context) {
id := c.Param("id")
objectId, _ := primitive.ObjectIDFromHex(id)
_, err := handler.collection.DeleteOne(handler.ctx, bson.M{"_id": objectId})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "delete success"})
}
// GET /recipes/:id
func (handler *RecipesHandler) GetOneRecipeHandler(c *gin.Context) {
id := c.Param("id")
objectId, _ := primitive.ObjectIDFromHex(id)
cur := handler.collection.FindOne(handler.ctx, bson.M{"_id": objectId})
var recipe models.Recipe
if err := cur.Decode(&recipe); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, recipe)
}Main Application
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"go.mongodb.org/mongo-driver/mongo/readpref"
"buildginapp/handlers"
)
var err error
var client *mongo.Client
var recipesHandler *handlers.RecipesHandler
func init() {
ctx := context.Background()
url := "mongodb://root:[email protected]:27017/demo?authSource=admin&maxPoolSize=20"
client, err = mongo.Connect(ctx, options.Client().ApplyURI(url))
if err = client.Ping(context.TODO(), readpref.Primary()); err != nil {
log.Fatal("connect to mongodb failed: ", err)
}
log.Println("Connected to MongoDB")
collection := client.Database("demo").Collection("recipes")
redisClient := redis.NewClient(&redis.Options{Addr: "192.168.0.20:6379", Password: "", DB: 0})
log.Println("redis ping: ", redisClient.Ping().String())
recipesHandler = handlers.NewRecipesHandler(ctx, collection, redisClient)
}
func main() {
gin.SetMode(gin.ReleaseMode)
router := gin.Default()
router.GET("/recipes", recipesHandler.ListRecipesHandler)
router.POST("/recipes", recipesHandler.NewRecipeHandler)
router.PUT("/recipes/:id", recipesHandler.UpdateRecipeHandler)
router.DELETE("/recipes/:id", recipesHandler.DeleteRecipeHandler)
router.GET("/recipes/:id", recipesHandler.GetOneRecipeHandler)
srv := &http.Server{Addr: "127.0.0.1:8080", Handler: router}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal("listen failed, ", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt, syscall.SIGTERM, syscall.SIGINT)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("server shutdown failed, err: %v
", err)
}
log.Println("server shutdown")
}Appendix – Data Preparation
package main
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"time"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"go.mongodb.org/mongo-driver/mongo/readpref"
)
type Recipe struct {
ID string `json:"id"`
Name string `json:"name"`
Tags []string `json:"tags"`
Ingredients []string `json:"ingredients"`
Instructions []string `json:"instructions"`
PublishedAt time.Time `json:"publishedAt"`
}
var recipes []Recipe
var ctx context.Context
var err error
var client *mongo.Client
func init() {
recipes = make([]Recipe, 0)
file, _ := ioutil.ReadFile("recipes.json")
json.Unmarshal([]byte(file), &recipes)
ctx = context.Background()
client, err = mongo.Connect(ctx, options.Client().ApplyURI("mongodb://root:[email protected]:27017/demo?authSource=admin"))
if err = client.Ping(context.TODO(), readpref.Primary()); err != nil {
log.Fatal(err)
}
collection := client.Database("demo").Collection("recipes")
insertManyResult, err := collection.InsertMany(ctx, toInterfaceSlice(recipes))
if err != nil {
log.Fatal(err)
}
log.Println("Inserted recipes: ", len(insertManyResult.InsertedIDs))
}
func toInterfaceSlice(r []Recipe) []interface{} {
s := make([]interface{}, len(r))
for i, v := range r {
s[i] = v
}
return s
}
func main() {
fmt.Println("insert many data to mongodb")
}Python Test Script
import requests
import json
def post_test(data):
url = "http://127.0.0.1:8080/recipes"
resp = requests.post(url, data=json.dumps(data))
print(resp.text)
def get_test():
url = "http://127.0.0.1:8080/recipes"
resp = requests.get(url)
print(resp.text)
def put_test(data, id):
url = f"http://127.0.0.1:8080/recipes/{id}"
resp = requests.put(url, data=json.dumps(data))
print(json.loads(resp.text))
def delete_test(id):
url = f"http://127.0.0.1:8080/recipes/{id}"
resp = requests.delete(url)
print("delete test")
print(resp.text)
def get_test_search(id):
url = f"http://127.0.0.1:8080/recipes/{id}"
resp = requests.get(url)
print("get test search")
print(resp.text)
if __name__ == "__main__":
data1 = {
"name": "Homemade Pizza",
"tags": ["italian", "pizza", "dinner"],
"ingredients": ["1 1/2 cups warm water", "1 package active dry yeast", "3 3/4 cups bread flour", "feta cheese, firm mozzarella cheese, grated"],
"instructions": ["Step 1.", "Step 2.", "Step 3."]
}
data2 = {
"name": "西红柿炒鸡蛋",
"tags": ["家常菜", "新手必会"],
"ingredients": ["2个鸡蛋", "1个番茄, 切片", "葱花, 蒜瓣等"],
"instructions": ["步骤1", "步骤2", "步骤3"]
}
data3 = {
"name": "蒸蛋",
"tags": ["家常菜", "新手必会"],
"ingredients": ["2个鸡蛋", "葱花, 蒜瓣等"],
"instructions": ["步骤1", "步骤2", "步骤3", "步骤4"]
}
# post_test(data1)
# post_test(data2)
# put_test(data2, id="62b7d298bb2ffa932f0d213d")
# get_test_search(id="62b6e5746202e6a3c26b0afb")
# delete_test(id="123456")
get_test()Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
MaGe Linux Operations
Founded in 2009, MaGe Education is a top Chinese high‑end IT training brand. Its graduates earn 12K+ RMB salaries, and the school has trained tens of thousands of students. It offers high‑pay courses in Linux cloud operations, Python full‑stack, automation, data analysis, AI, and Go high‑concurrency architecture. Thanks to quality courses and a solid reputation, it has talent partnerships with numerous internet firms.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
