In this article, you’ll learn how to build a CRUD RESTful API server with Golang, Gin Gonic, MongoDB-Go-driver, Docker, and Docker-compose.

CRUD RESTful API with Golang + MongoDB Series:

  1. API with Golang + MongoDB + Redis + Gin Gonic: Project Setup
  2. Golang & MongoDB: JWT Authentication and Authorization
  3. API with Golang + MongoDB: Send HTML Emails with Gomail
  4. API with Golang, Gin Gonic & MongoDB: Forget/Reset Password
  5. Build Golang gRPC Server and Client: SignUp User & Verify Email
  6. Build Golang gRPC Server and Client: Access & Refresh Tokens
  7. Build CRUD RESTful API Server with Golang, Gin, and MongoDB
Build CRUD RESTful API Server with Golang, Gin, and MongoDB

Golang, Gin Gonic, MongoDB CRUD RESTful API Overview

We’ll build a RESTful API where we can create, read, update and delete a post in the MongoDB database.

RESOURCEHTTP METHODROUTEDESCRIPTION
postsGET/api/postsRetrieve all posts
postsPOST/api/postsCreates a new post
postsGET/api/posts/:postIdRetrieve a single post
postsPATCH/api/posts/:postIdUpdate a post
postsDELETE/api/posts/:postIdDelete a post

Am going to use Postman to test the Golang API endpoints. You can use any API testing tool you are comfortable with.

-To create a new post, you need to make a POST request to /api/posts

golang crud restful api create new post

-Update a single post by making a PATCH request to /api/posts/:postId

updated a post golang crud restful api with postman

-Retrieve a single post by making a GET request to /api/posts/:postId

get a single post golang crud restful api with postman

-Delete a post by making a DELETE request to /api/posts/:postId

delete a post golang crud restful api with postman

-Retrieve all posts by making a GET request to /api/posts?page=1&limit=10

golang crud restful api fetch all posts

-You should see all the posts you created in the MongoDB database.

get all the posts in the mongodb collection

Create the Models with Structs

The most obvious way to begin is to create the Golang structs to represent the models we’ll be working with. These structs need to have the BSON annotation tags.

models/post.model.go


package models

import (
	"time"

	"go.mongodb.org/mongo-driver/bson/primitive"
)

type CreatePostRequest struct {
	Title     string    `json:"title" bson:"title" binding:"required"`
	Content   string    `json:"content" bson:"content" binding:"required"`
	Image     string    `json:"image,omitempty" bson:"image,omitempty"`
	User      string    `json:"user" bson:"user" binding:"required"`
	CreateAt  time.Time `json:"created_at,omitempty" bson:"created_at,omitempty"`
	UpdatedAt time.Time `json:"updated_at,omitempty" bson:"updated_at,omitempty"`
}

type DBPost struct {
	Id        primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"`
	Title     string             `json:"title,omitempty" bson:"title,omitempty"`
	Content   string             `json:"content,omitempty" bson:"content,omitempty"`
	Image     string             `json:"image,omitempty" bson:"image,omitempty"`
	User      string             `json:"user,omitempty" bson:"user,omitempty"`
	CreateAt  time.Time          `json:"created_at,omitempty" bson:"created_at,omitempty"`
	UpdatedAt time.Time          `json:"updated_at,omitempty" bson:"updated_at,omitempty"`
}

type UpdatePost struct {
	Id        primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"`
	Title     string             `json:"title,omitempty" bson:"title,omitempty"`
	Content   string             `json:"content,omitempty" bson:"content,omitempty"`
	Image     string             `json:"image,omitempty" bson:"image,omitempty"`
	User      string             `json:"user,omitempty" bson:"user,omitempty"`
	CreateAt  time.Time          `json:"created_at,omitempty" bson:"created_at,omitempty"`
	UpdatedAt time.Time          `json:"updated_at,omitempty" bson:"updated_at,omitempty"`
}


In some of the structs, we provided the Id fields with a primitive.ObjectID type. This will inform the MongoDB server that these fields will be actual MongoDB ObjectIds.

In addition, the _id bson fields of the Structs will be filled with unique ObjectIds by the MongoDB Server.

You will notice that in the CreatePostRequest struct, we omitted the Id field. That is because when inserting a new document into the MongoDB database, we don’t need to provide the Id field since the MongoDB server will automatically generate and assign a unique ObjectId to the document.

Also, in the CreatePostRequest struct, we added the required annotation to some of the fields to help Gin Gonic validate the request body against the provided struct and return the appropriate validation errors if any of the required fields were not provided.

Finally, we used the time.Time data type for the created_at and updated_at fields. This will tell the MongoDB server that these fields should have the bson timestamp data type.

Create the Service Interface

One of the RESTful API architectures is to separate the business from the application logic.

We always push most of the business logic to the models or services so that we can end up with fat models or services and thin controllers.

To begin, let’s define an interface to list the methods we must implement to perform the basic CRUD operations.

services/post.service.go


type PostService interface {
	CreatePost(*models.CreatePostRequest) (*models.DBPost, error)
	UpdatePost(string, *models.UpdatePost) (*models.DBPost, error)
	FindPostById(string) (*models.DBPost, error)
	FindPosts(page int, limit int) ([]*models.DBPost, error)
	DeletePost(string) error
}

Create Methods to Implement the Interface

Next, let’s create a struct to help us implement the PostService interface we defined above.

We’re going to need two key things – the MongoDB collection struct to help us interact with the MongoDB server and the context interface to perform some background cleanup.

Initialize the Service Struct

Since Golang does not support inheritance, we’ll use a popular programming paradigm called composition to inherit the features of the MongoDB collection struct and the context interface.

services/post.service.impl.go


type PostServiceImpl struct {
	postCollection *mongo.Collection
	ctx            context.Context
}

func NewPostService(postCollection *mongo.Collection, ctx context.Context) PostService {
	return &PostServiceImpl{postCollection, ctx}
}

Define a Service to Create a Post

Now let’s create a service to insert the new post into the MongoDB database. To do that, we’ll use a receiver function also known as a method to set the CreatePost method on the PostServiceImpl struct.

The MongoDB InsertOne() method accepts a context and an interface which in this case is the CreatePostRequest struct. The MongoDB InsertOne() function will automatically convert the struct into BSON using the tags we provided.

services/post.service.impl.go


func (p *PostServiceImpl) CreatePost(post *models.CreatePostRequest) (*models.DBPost, error) {
	post.CreateAt = time.Now()
	post.UpdatedAt = post.CreateAt
	res, err := p.postCollection.InsertOne(p.ctx, post)

	if err != nil {
		if er, ok := err.(mongo.WriteException); ok && er.WriteErrors[0].Code == 11000 {
			return nil, errors.New("post with that title already exists")
		}
		return nil, err
	}

	opt := options.Index()
	opt.SetUnique(true)

	index := mongo.IndexModel{Keys: bson.M{"title": 1}, Options: opt}

	if _, err := p.postCollection.Indexes().CreateOne(p.ctx, index); err != nil {
		return nil, errors.New("could not create index for title")
	}

	var newPost *models.DBPost
	query := bson.M{"_id": res.InsertedID}
	if err = p.postCollection.FindOne(p.ctx, query).Decode(&newPost); err != nil {
		return nil, err
	}

	return newPost, nil
}

In the above code, I added the createdAt and updatedAt timestamps to the post before inserting it into the database.

Also, I added a unique constraint on the title field so that no two posts will have the same title.

Next, I used the ObjectId returned by the InsertOne() method to retrieve the newly created document.

Define a Service to Update Post

To perform an update operation, we use the MongoDB $set operator. If you will remember, we added an omitempty annotation to all the fields in the UpdatePost struct to help us update only the fields provided in the request body.

Another important thing is that the MongoDB FindOneAndUpdate() method returns the document as it appeared before updating, so we need to explicitly instruct it to return the newly updated document.

services/post.service.impl.go


func (p *PostServiceImpl) UpdatePost(id string, data *models.UpdatePost) (*models.DBPost, error) {
	doc, err := utils.ToDoc(data)
	if err != nil {
		return nil, err
	}

	obId, _ := primitive.ObjectIDFromHex(id)
	query := bson.D{{Key: "_id", Value: obId}}
	update := bson.D{{Key: "$set", Value: doc}}
	res := p.postCollection.FindOneAndUpdate(p.ctx, query, update, options.FindOneAndUpdate().SetReturnDocument(1))

	var updatedPost *models.DBPost

	if err := res.Decode(&updatedPost); err != nil {
		return nil, errors.New("no post with that Id exists")
	}

	return updatedPost, nil
}

Define a Service to Delete Post

Now let’s perform the simplest CRUD operation. To delete a document in the collection, we use the MongoDB DeleteOne() or FindOneAndDelete() methods.

The FindOneAndDelete() method returns the document as it appeared before deletion whilst the DeleteOne() method returns a DeletedCount value.

services/post.service.impl.go


func (p *PostServiceImpl) DeletePost(id string) error {
	obId, _ := primitive.ObjectIDFromHex(id)
	query := bson.M{"_id": obId}

	res, err := p.postCollection.DeleteOne(p.ctx, query)
	if err != nil {
		return err
	}

	if res.DeletedCount == 0 {
		return errors.New("no document with that Id exists")
	}

	return nil
}

Define a Service to Get Single Post

Next, let’s define a service to return a single document from the collection. The MongoDB FindOne() method accepts a context and a filter. The filter can be an unordered or ordered BSON document with key/value pairs.

In addition, we need to decode the found document to a regular Golang struct.

services/post.service.impl.go


func (p *PostServiceImpl) FindPostById(id string) (*models.DBPost, error) {
	obId, _ := primitive.ObjectIDFromHex(id)

	query := bson.M{"_id": obId}

	var post *models.DBPost

	if err := p.postCollection.FindOne(p.ctx, query).Decode(&post); err != nil {
		if err == mongo.ErrNoDocuments {
			return nil, errors.New("no document with that Id exists")
		}

		return nil, err
	}

	return post, nil
}

Define a Service to Get All Posts

The last CRUD operation is a little challenging so pay attention. MongoDB also has a cursor feature which is simply a pointer to the results of a query rather than all the results in memory.

With the help of a for loop and the cursor.Next() method, we’ll iterate through the cursor to retrieve the documents and append each document to a defined slice.

Also, remember to close the cursor with the cursor.Close() method before returning from the FindPosts() function.

services/post.service.impl.go


func (p *PostServiceImpl) FindPosts(page int, limit int) ([]*models.DBPost, error) {
        if page == 0 {
		page = 1
	}

	if limit == 0 {
		limit = 10
	}

	skip := (page - 1) * limit

	opt := options.FindOptions{}
	opt.SetLimit(int64(limit))
	opt.SetSkip(int64(skip))

	query := bson.M{}

	cursor, err := p.postCollection.Find(p.ctx, query, &opt)
	if err != nil {
		return nil, err
	}

	defer cursor.Close(p.ctx)

	var posts []*models.DBPost

	for cursor.Next(p.ctx) {
		post := &models.DBPost{}
		err := cursor.Decode(post)

		if err != nil {
			return nil, err
		}

		posts = append(posts, post)
	}

	if err := cursor.Err(); err != nil {
		return nil, err
	}

	if len(posts) == 0 {
		return []*models.DBPost{}, nil
	}

	return posts, nil
}

Create Controllers to Perform the CRUD Operations

Once the services are ready, it’s time to perform the actual CRUD operations by calling the appropriate service to access and mutate the database.

Initialize the Controller Struct

Since we are dealing with the PostService, let’s use composition to inherit all the methods we defined above in our own struct.

controllers/post.controller.go


type PostController struct {
	postService services.PostService
}

func NewPostController(postService services.PostService) PostController {
	return PostController{postService}
}

Define a Controller to Create a Post

To validate the request body, Gin Gonic provides us with ShouldBindJSON() method that accepts an interface which in this case is the CreatePostRequest struct and returns a validation error to the user based on the rules specified in the binding tag.

With the validation out of the way, we can now call the CreatePost() service we defined above to add the new post to the MongoDB database.

controllers/post.controller.go


func (pc *PostController) CreatePost(ctx *gin.Context) {
	var post *models.CreatePostRequest

	if err := ctx.ShouldBindJSON(&post); err != nil {
		ctx.JSON(http.StatusBadRequest, err.Error())
		return
	}

	newPost, err := pc.postService.CreatePost(post)

	if err != nil {
		if strings.Contains(err.Error(), "title already exists") {
			ctx.JSON(http.StatusConflict, gin.H{"status": "fail", "message": err.Error()})
			return
		}

		ctx.JSON(http.StatusBadGateway, gin.H{"status": "fail", "message": err.Error()})
		return
	}

	ctx.JSON(http.StatusCreated, gin.H{"status": "success", "data": newPost})
}

Define a Controller to Update a Post

Updating a post is nearly similar to creating a post, with the addition of the postId parameter, since we need to tell the MongoDB server what document to update.

To update a post, we first need to extract the postId from the request parameter and pass it along with the request body to the UpdatePost service.

controllers/post.controller.go


func (pc *PostController) UpdatePost(ctx *gin.Context) {
	postId := ctx.Param("postId")

	var post *models.UpdatePost
	if err := ctx.ShouldBindJSON(&post); err != nil {
		ctx.JSON(http.StatusBadGateway, gin.H{"status": "fail", "message": err.Error()})
		return
	}

	updatedPost, err := pc.postService.UpdatePost(postId, post)
	if err != nil {
		if strings.Contains(err.Error(), "Id exists") {
			ctx.JSON(http.StatusNotFound, gin.H{"status": "fail", "message": err.Error()})
			return
		}
		ctx.JSON(http.StatusBadGateway, gin.H{"status": "fail", "message": err.Error()})
		return
	}

	ctx.JSON(http.StatusOK, gin.H{"status": "success", "data": updatedPost})
}

Define a Controller to Delete a Post

Am always happy when I reach the delete operation since it’s the simplest of all the CRUD operations.

To delete a document from the collection, you need to retrieve the postId from the request parameter and pass it to the DeletePost() service.

controllers/post.controller.go


func (pc *PostController) DeletePost(ctx *gin.Context) {
	postId := ctx.Param("postId")

	err := pc.postService.DeletePost(postId)

	if err != nil {
		if strings.Contains(err.Error(), "Id exists") {
			ctx.JSON(http.StatusNotFound, gin.H{"status": "fail", "message": err.Error()})
			return
		}
		ctx.JSON(http.StatusBadGateway, gin.H{"status": "fail", "message": err.Error()})
		return
	}

	ctx.JSON(http.StatusNoContent, nil)
}


Define a Controller to Get a Single Post

The read CRUD operation follows the same strategy to delete a post. Here, you provide the FindPostById() service with the postId to retrieve the post that matches that Id.

controllers/post.controller.go


func (pc *PostController) FindPostById(ctx *gin.Context) {
	postId := ctx.Param("postId")

	post, err := pc.postService.FindPostById(postId)

	if err != nil {
		if strings.Contains(err.Error(), "Id exists") {
			ctx.JSON(http.StatusNotFound, gin.H{"status": "fail", "message": err.Error()})
			return
		}
		ctx.JSON(http.StatusBadGateway, gin.H{"status": "fail", "message": err.Error()})
		return
	}

	ctx.JSON(http.StatusOK, gin.H{"status": "success", "data": post})
}

Define a Controller to Get All Posts

If we want to retrieve all the posts, the JSON payload might be too large if we have a lot of posts in the database.

To fix this, I added a pagination feature to allow the user to specify the number of documents to skip and the number of documents to return.

controllers/post.controller.go


func (pc *PostController) FindPosts(ctx *gin.Context) {
	var page = ctx.DefaultQuery("page", "1")
	var limit = ctx.DefaultQuery("limit", "10")

	intPage, err := strconv.Atoi(page)
	if err != nil {
		ctx.JSON(http.StatusBadGateway, gin.H{"status": "fail", "message": err.Error()})
		return
	}

	intLimit, err := strconv.Atoi(limit)
	if err != nil {
		ctx.JSON(http.StatusBadGateway, gin.H{"status": "fail", "message": err.Error()})
		return
	}

	posts, err := pc.postService.FindPosts(intPage, intLimit)
	if err != nil {
		ctx.JSON(http.StatusBadGateway, gin.H{"status": "fail", "message": err.Error()})
		return
	}

	ctx.JSON(http.StatusOK, gin.H{"status": "success", "results": len(posts), "data": posts})
}

Create the Routes for the Controllers

Now let’s use the power of composition to inherit all the methods of the PostController struct we defined above.

Next, let’s create all the CRUD routes to call the controllers.

routes/post.routes.go


type PostRouteController struct {
	postController controllers.PostController
}

func NewPostControllerRoute(postController controllers.PostController) PostRouteController {
	return PostRouteController{postController}
}

func (r *PostRouteController) PostRoute(rg *gin.RouterGroup) {
	router := rg.Group("/posts")

	router.GET("/", r.postController.FindPosts)
	router.GET("/:postId", r.postController.FindPostById)
	router.POST("/", r.postController.CreatePost)
	router.PATCH("/:postId", r.postController.UpdatePost)
	router.DELETE("/:postId", r.postController.DeletePost)
}

Initialize the Constructors in the Main file

The final piece of the puzzle is to evoke all the constructors we defined above. I left some comments in the code snippets below to help you.

Focus only on the Gin server.

cmd/server/main.go


package main

import (
	"context"
	"fmt"
	"log"
	"net"
	"net/http"

	"github.com/gin-contrib/cors"
	"github.com/gin-gonic/gin"
	"github.com/go-redis/redis/v8"
	"github.com/wpcodevo/golang-mongodb/config"
	"github.com/wpcodevo/golang-mongodb/controllers"
	"github.com/wpcodevo/golang-mongodb/gapi"
	"github.com/wpcodevo/golang-mongodb/pb"
	"github.com/wpcodevo/golang-mongodb/routes"
	"github.com/wpcodevo/golang-mongodb/services"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
	"go.mongodb.org/mongo-driver/mongo/readpref"
	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"
)

var (
	server      *gin.Engine
	ctx         context.Context
	mongoclient *mongo.Client
	redisclient *redis.Client

	userService         services.UserService
	UserController      controllers.UserController
	UserRouteController routes.UserRouteController

	authCollection      *mongo.Collection
	authService         services.AuthService
	AuthController      controllers.AuthController
	AuthRouteController routes.AuthRouteController

	// ? Add the Post Service, Controllers and Routes
	postService         services.PostService
	PostController      controllers.PostController
	postCollection      *mongo.Collection
	PostRouteController routes.PostRouteController
)

func init() {
	config, err := config.LoadConfig(".")
	if err != nil {
		log.Fatal("Could not load environment variables", err)
	}

	ctx = context.TODO()

	// Connect to MongoDB
	mongoconn := options.Client().ApplyURI(config.DBUri)
	mongoclient, err := mongo.Connect(ctx, mongoconn)

	if err != nil {
		panic(err)
	}

	if err := mongoclient.Ping(ctx, readpref.Primary()); err != nil {
		panic(err)
	}

	fmt.Println("MongoDB successfully connected...")

	// Connect to Redis
	redisclient = redis.NewClient(&redis.Options{
		Addr: config.RedisUri,
	})

	if _, err := redisclient.Ping(ctx).Result(); err != nil {
		panic(err)
	}

	err = redisclient.Set(ctx, "test", "Welcome to Golang with Redis and MongoDB", 0).Err()
	if err != nil {
		panic(err)
	}

	fmt.Println("Redis client connected successfully...")

	// Collections
	authCollection = mongoclient.Database("golang_mongodb").Collection("users")
	userService = services.NewUserServiceImpl(authCollection, ctx)
	authService = services.NewAuthService(authCollection, ctx)
	AuthController = controllers.NewAuthController(authService, userService, ctx, authCollection)
	AuthRouteController = routes.NewAuthRouteController(AuthController)

	UserController = controllers.NewUserController(userService)
	UserRouteController = routes.NewRouteUserController(UserController)

	// ? Add the Post Service, Controllers and Routes
	postCollection = mongoclient.Database("golang_mongodb").Collection("posts")
	postService = services.NewPostService(postCollection, ctx)
	PostController = controllers.NewPostController(postService)
	PostRouteController = routes.NewPostControllerRoute(PostController)

	server = gin.Default()
}

func main() {
	config, err := config.LoadConfig(".")

	if err != nil {
		log.Fatal("Could not load config", err)
	}

	defer mongoclient.Disconnect(ctx)

	startGinServer(config)
	// startGrpcServer(config)
}

func startGrpcServer(config config.Config) {
	authServer, err := gapi.NewGrpcAuthServer(config, authService, userService, authCollection)
	if err != nil {
		log.Fatal("cannot create grpc authServer: ", err)
	}

	userServer, err := gapi.NewGrpcUserServer(config, userService, authCollection)
	if err != nil {
		log.Fatal("cannot create grpc userServer: ", err)
	}

	grpcServer := grpc.NewServer()

	pb.RegisterAuthServiceServer(grpcServer, authServer)
	pb.RegisterUserServiceServer(grpcServer, userServer)
	reflection.Register(grpcServer)

	listener, err := net.Listen("tcp", config.GrpcServerAddress)
	if err != nil {
		log.Fatal("cannot create grpc server: ", err)
	}

	log.Printf("start gRPC server on %s", listener.Addr().String())
	err = grpcServer.Serve(listener)
	if err != nil {
		log.Fatal("cannot create grpc server: ", err)
	}
}

func startGinServer(config config.Config) {
	value, err := redisclient.Get(ctx, "test").Result()

	if err == redis.Nil {
		fmt.Println("key: test does not exist")
	} else if err != nil {
		panic(err)
	}

	corsConfig := cors.DefaultConfig()
	corsConfig.AllowOrigins = []string{config.Origin}
	corsConfig.AllowCredentials = true

	server.Use(cors.New(corsConfig))

	router := server.Group("/api")
	router.GET("/healthchecker", func(ctx *gin.Context) {
		ctx.JSON(http.StatusOK, gin.H{"status": "success", "message": value})
	})

	AuthRouteController.AuthRoute(router, userService)
	UserRouteController.UserRoute(router, userService)
	// ? Evoke the PostRoute
	PostRouteController.PostRoute(router)
	log.Fatal(server.Run(":" + config.Port))
}

Conclusion

Congrats for reaching the end. In this comprehensive article, you learned how to build a CRUD API server with Golang, MongoDB-Go-driver, Gin Gonic, Docker, and Docker-compose.

Check out the source code on GitHub