In this article, you’ll learn how to build a CRUD gRPC API server with Golang, MongoDB-Go-driver, and Docker-compose. You’ll also build a gRPC client to interact with the gRPC API.

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
  8. Build CRUD gRPC Server API & Client with Golang and MongoDB
Build CRUD gRPC Server API & Client with Golang and MongoDB

What the course will cover

  • How to create a gRPC server to perform CRUD operations.
  • How to create a gRPC client to perform CRUD operations
  • How to interact with the gRPC server using Evans CLI

Prerequisites

Software

VS Code Extensions

  • DotENV – To get syntax highlighting in the .env file.
  • Proto3 – To get syntax highlighting, syntax validation, and code formatting in the .proto files.
  • MySQL – A GUI to view the data stored in the MongoDB database.

Define the Models with Structs

To begin, we first need to create structs to represent the data models we’ll be working with. The fields on the structs need to have JSON and BSON annotation tags.

The JSON tags will become relevant when we implement gRPC-Gateway. The gRPC-Gateway plugin of Protoc will help us generate a reverse proxy server for the gRPC services to handle both RESTful/JSON and gRPC requests and Responses.

The BSON tags on the other hand will be used by the MongoDB driver to serialize the data before sending it to the MongoDB server.

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 {
	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"`
}


The primitive.ObjectID type used for the Id field will inform the MongoDB server that it will eventually be an ObjectId.

Also, the _id BSON annotation will be automatically filled with a unique ObjectId by the MongoDB Server.

Since the MongoDB server automatically assigns a unique ObjectId to every newly created document, there is no including an Id field in the CreatePostRequest struct.

Lastly, when dealing with timestamps in Golang, we use the time.Time datatype. Having a time.Time datatype on the created_at and updated_at fields will instruct the MongoDB server to assign a BSON timestamp datatype to these fields.

Create the ProtoBuf Messages

To begin, we need to create a proto file describing the gRPC services and RPC methods we want to use remotely using the protocol buffer language.

To avoid polluting the root directory with a bunch of proto files, it’s recommended to keep all the .proto files in a folder.

Create a proto/post.proto file to contain the request and response messages of a post.

The outline of a proto file is usually one-way, we start by specifying the version of the Protocol Buffer Language which in this tutorial is proto3 followed by the package name.

Next, we use the option keyword to tell the Protocol Buffer Compiler where we want to put the generated stubs or interfaces.

Normally, the number of option statements required depends on the language you are generating the stubs or interfaces for.

To define a ProtoBuf message field, we start with the field type, followed by the field name, and assign it a unique number that will be used by the gRPC framework to identify it in the message binary format.

ProtoBuf does not come with a timestamp type but since it’s a well-known datatype, Google has already added it to their standard library.

To use the google.protobuf.Timestamp type, you need to import it into your proto file and assign it to the fields that need to have timestamp type.

proto/post.proto


syntax = "proto3";

package pb;

option go_package = "github.com/wpcodevo/golang-mongodb/pb";
import "google/protobuf/timestamp.proto";

message Post {
  string Id = 1;
  string Title = 2;
  string Content = 3;
  string Image = 4;
  string User = 5;
  google.protobuf.Timestamp created_at = 6;
  google.protobuf.Timestamp updated_at = 7;
}

message PostResponse { Post post = 1; }

To create a post, we require four fields – the post title, content, image, and the user’s ID. There is no ID field since MongoDB will automatically assign the inserted document a unique ObjectId.

proto/rpc_create_post.proto


syntax = "proto3";

package pb;

option go_package = "github.com/wpcodevo/golang-mongodb/pb";

message CreatePostRequest {
  string Title = 1;
  string Content = 2;
  string Image = 3;
  string User = 4;
}


Very similar to CreatePostRequest, but the UpdatePostRequest will contain an ID to find and update that specific document in the database. The remaining fields should be optional to give the user the freedom to update any field.

proto/rpc_update_post.proto


syntax = "proto3";

package pb;

option go_package = "github.com/wpcodevo/golang-mongodb/pb";

message UpdatePostRequest {
  string Id = 1;
  optional string Title = 2;
  optional string Content = 3;
  optional string Image = 4;
  optional string User = 5;
}


Define the gRPC Service and RPC Methods

We’ll need 5 RPC methods, each method accepts a request and returns a response:

  • CreatePost (Unary RPC)
  • GetPost (Unary RPC)
  • GetPosts (Server streaming RPC)
  • UpdatePost (Unary RPC)
  • DeletePost (Unary RPC)

To avoid sending a huge ProtoBuf message to the client, the GetPosts RPC service has a pagination feature that accepts an optional page number and the maximum number of documents to retrieve from the MongoDB database.

The DeletePost RPC is similar to GetPost , we find the post by ID and remove it from the database. Assuming that ID exists in the database, the DeletePost RPC will return a boolean of true .

proto/post_service.proto


syntax = "proto3";

package pb;

option go_package = "github.com/wpcodevo/golang-mongodb/pb";
import "post.proto";
import "rpc_create_post.proto";
import "rpc_update_post.proto";

service PostService {
  rpc CreatePost(CreatePostRequest) returns (PostResponse) {}
  rpc GetPost(PostRequest) returns (PostResponse) {}
  rpc GetPosts(GetPostsRequest) returns (stream Post) {}
  rpc UpdatePost(UpdatePostRequest) returns (PostResponse) {}
  rpc DeletePost(PostRequest) returns (DeletePostResponse) {}
}

message GetPostsRequest {
  optional int64 page = 1;
  optional int64 limit = 2;
}

message PostRequest { string Id = 1; }

message DeletePostResponse { bool success = 1; }

Generate the Golang gRPC Code

Since we’ve created the .proto definition files, it’s now time to generate the client and server stubs or interfaces in Golang. To achieve this we’ll be using the Protocol Buffer compiler combined with the Protoc-gen-go plugin.

Once the installation is successful, create a proto-gen.sh file in the root directory and paste the code snippets below into it.

proto-gen.sh


#!/bin/bash

rm -rf pb/*.go
protoc --proto_path=proto --go_out=pb --go_opt=paths=source_relative \
  --go-grpc_out=pb --go-grpc_opt=paths=source_relative \
  proto/*.proto

Note: Create a pb folder in the root directory for the compilation process to work.

In the above, Protoc will compile the .proto files and output them into the pb folder.

Open the pb folder and you’ll notice that each .proto file we created in the proto folder has its corresponding Golang file.

Each file in the pb folder has a .pb.go extension with one special file having _grpc.pb.go extension.

The _grpc.pb.go file contains the types and interfaces of the server and client. It also has the methods to create a new server and client.

Define a Custom Service Interface

To avoid bloating the gRPC controllers, let’s create some services to interact with the MongoDB database.

To do that, let’s define an interface that we must implement to perform the CRUD operations against the database.

services/post.service.go


package services

import "github.com/wpcodevo/golang-mongodb/models"

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 Service Interface

Next, let’s define a struct to inherit the MongoDB collection struct and the context interface. The MongoDB collection struct contains the methods needed to perform the CRUD operations.

The context interface on the other hand will help us perform some background cleanups to avoid wasting resources.

Create a Constructor to Implement the Service Interface

Below, I used a popular programming paradigm known as composition to inherit the features of the MongoDB collection and the context interface since Golang does not support inheritance.

services/post.service.impl.go


package services

import (
	"context"
	"errors"
	"time"

	"github.com/wpcodevo/golang-mongodb/models"
	"github.com/wpcodevo/golang-mongodb/utils"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
)

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

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

Create a new Post

Now let’s define a service to add the new post to the MongoDB database. To do that, we’ll create a method on the PostServiceImpl struct.

The InsertOne() of MongoDB takes a context and an interface which in this case is the post struct. The InsertOne() method automatically converts the struct into BSON using the provided tags.

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, I added the createdAt and updatedAt timestamps to the post before inserting it into the MongoDB database.

In addition, I added a unique constraint on the title field to ensure that no two posts should end up with the same title.

Finally, I used the ObjectId returned by the InsertOne() function to retrieve the newly created post.

Update a Post

When performing an update operation on the MongoDB database, we use the $set operator.

If you remember, we added an omitempty annotation to the BSON tags in the UpdatePost struct to enable us to update only the fields provided in the request body.

The MongoDB FindOneAndUpdate() function returns the document as it appeared before updating, so we need to explicitly tell it to return the 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
}

Find a Post

To retrieve a post from the database, we can use any of the fields defined in the struct but we’ll use the ID since it’s unique. The FindOne() function of MongoDB accepts a filter and a context.

The filter is simple a BSON document having key/value pairs. Once a document matches the filter, we need to decode the found document to a standard 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
}

Retrieve All Posts

When we call the Find() method, MongoDB returns a cursor which is a pointer to the results of the query in memory.

To retrieve all the posts, we need to loop through the cursor with the help of the cursor.Next() method and append each document to a defined slice.

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))
	opt.SetSort(bson.M{"created_at": -1})

	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
}

Delete a Post

To delete a post in the database, we can use either DeleteOne() or FindOneAndDelete() methods.

The FindOneAndDelete() function returns the document as it appeared before deletion where as the DeleteOne() function 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 the gRPC Controllers

After defining the services above, it’s now time to perform the actual CRUD operations by evoking the right service to query or mutate the database.

Server Implementation

In the root directory, create a gapi/post-server.go file to contain the code needed to handle the gRPC requests.

In the pb folder, open the post_service_grpc.pb.go file and you should see a PostServiceServer interface that we must implement to serve gRPC requests on the server.

pb/post_service_grpc.pb.go


type PostServiceServer interface {
	CreatePost(context.Context, *CreatePostRequest) (*PostResponse, error)
	GetPost(context.Context, *PostRequest) (*PostResponse, error)
	GetPosts(*GetPostsRequest, PostService_GetPostsServer) error
	UpdatePost(context.Context, *UpdatePostRequest) (*PostResponse, error)
	DeletePost(context.Context, *PostRequest) (*DeletePostResponse, error)
	mustEmbedUnimplementedPostServiceServer()
}

In current versions of gRPC, Protoc generates an unimplemented server struct where all the RPCs are already defined but they return an unimplemented error.

gapi/post-server.go


package gapi

import (
	"github.com/wpcodevo/golang-mongodb/pb"
	"github.com/wpcodevo/golang-mongodb/services"
	"go.mongodb.org/mongo-driver/mongo"
)

type PostServer struct {
	pb.UnimplementedPostServiceServer
	postCollection *mongo.Collection
	postService    services.PostService
}

func NewGrpcPostServer(postCollection *mongo.Collection, postService services.PostService) (*PostServer, error) {
	postServer := &PostServer{
		postCollection: postCollection,
		postService:    postService,
	}

	return postServer, nil
}


Update the Server Main File

Now let’s start a new gRPC server and register our Post service. The pb package has a method called RegisterPostServiceServer that accepts a pointer to a fresh gRPC server and a struct that implements the PostServiceServer interface.

To start and hot-reload the server, I will use the Golang Air package.

After installing the Air package, run air init in the terminal to generate a config file in the root directory.

Update the cmd code in the generated Air config file to:


cmd = "go build -o ./tmp/main.exe ./cmd/server/main.go"

Next, update the cmd/server/main.go file with the code below. I left some comments in the code to help you.

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

	// ? Create the Post Variables
	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)

	// ? Instantiate the Constructors
	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)
	}

	postServer, err := gapi.NewGrpcPostServer(postCollection, postService)
	if err != nil {
		log.Fatal("cannot create grpc postServer: ", err)
	}

	grpcServer := grpc.NewServer()

	pb.RegisterAuthServiceServer(grpcServer, authServer)
	pb.RegisterUserServiceServer(grpcServer, userServer)
	// ? Register the Post gRPC service
	pb.RegisterPostServiceServer(grpcServer, postServer)
	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)
	// ? Post Route
	PostRouteController.PostRoute(router)
	log.Fatal(server.Run(":" + config.Port))
}


Run the following command to start the gRPC server on port 8080.


air

Test the gRPC API Server with Evans CLI

Evans is a gRPC client used for sending gRPC requests in an interactive console.

Navigate to Evans CLI GitHub page to download and install the binary for Mac, Linux, or windows.

Am on a Windows machine so I installed it with the go install option.

Since the server is enabling gRPC reflection, you can launch the Evans console with the -r option.

Run the command below to connect the Evans console to the server.


evans --host localhost --port 8080 -r repl

In the Evans console, run show service to list all services and RPCs available on the server.

golang evans cli console

Next, type service then press the spacebar and press tab. Now, use the up/down arrow keys to select the PostService and press enter.

Next, type call then press the spacebar and press tab. Then use the up/down arrow keys to select the CreatePost method and press enter.

Next, provide the required fields and hit the enter key. You should get an unimplemented error from the server since we haven’t implemented the CreatePost RPC method yet.

evans unimplemented error to create a post

However, this indicates that the gRPC server is working and is ready to accept gRPC requests from the client.

Follow the same steps to call the GetPost method:

evans get a single post

Now that the gRPC server is ready to accept requests, it’s time to implement the RPC methods we defined in the post_service.proto file. The methods will accept the PostServer struct we defined above as receiver.

Create the gRPC API Handlers in Golang

In this section, we will create the gRPC API handlers that will be evoked to handle the requests.

CreatePost gRPC Handler

Now let’s implement the CreatePost method. Extract the fields from the request message and provide them to the CreatePostRequest struct.

Next, call the CreatePost() service to add the new post to the MongoDB database.

Once the post has been inserted into the database, construct the PostResponse ProtoBuf message with the result returned by the CreatePost service and send the message to the client.

gapi/rpc_create_post.go


package gapi

import (
	"context"
	"strings"

	"github.com/wpcodevo/golang-mongodb/models"
	"github.com/wpcodevo/golang-mongodb/pb"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/timestamppb"
)

func (postServer *PostServer) CreatePost(ctx context.Context, req *pb.CreatePostRequest) (*pb.PostResponse, error) {

	post := &models.CreatePostRequest{
		Title:   req.GetTitle(),
		Content: req.GetContent(),
		Image:   req.GetImage(),
		User:    req.GetUser(),
	}

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

	if err != nil {
		if strings.Contains(err.Error(), "title already exists") {
			return nil, status.Errorf(codes.AlreadyExists, err.Error())
		}

		return nil, status.Errorf(codes.Internal, err.Error())
	}

	res := &pb.PostResponse{
		Post: &pb.Post{
			Id:        newPost.Id.Hex(),
			Title:     newPost.Title,
			Content:   newPost.Content,
			User:      newPost.User,
			CreatedAt: timestamppb.New(newPost.CreateAt),
			UpdatedAt: timestamppb.New(newPost.UpdatedAt),
		},
	}
	return res, nil
}

UpdatePost gRPC Service Handler

To update a post in the database, we’ll call the UpdatePost() service and provide it with the ID of the post and a struct containing the fields to update.

If the update is successful, we construct the PostResponse ProtoBuf message with the updated post and return it to the client.

gapi/rpc_update_post.go


package gapi

import (
	"context"
	"strings"
	"time"

	"github.com/wpcodevo/golang-mongodb/models"
	"github.com/wpcodevo/golang-mongodb/pb"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/timestamppb"
)

func (postServer *PostServer) UpdatePost(ctx context.Context, req *pb.UpdatePostRequest) (*pb.PostResponse, error) {
	postId := req.GetId()

	post := &models.UpdatePost{
		Title:     req.GetTitle(),
		Content:   req.GetContent(),
		Image:     req.GetImage(),
		User:      req.GetUser(),
		UpdatedAt: time.Now(),
	}

	updatedPost, err := postServer.postService.UpdatePost(postId, post)

	if err != nil {
		if strings.Contains(err.Error(), "Id exists") {
			return nil, status.Errorf(codes.NotFound, err.Error())
		}
		return nil, status.Errorf(codes.Internal, err.Error())
	}

	res := &pb.PostResponse{
		Post: &pb.Post{
			Id:        updatedPost.Id.Hex(),
			Title:     updatedPost.Title,
			Content:   updatedPost.Content,
			Image:     updatedPost.Image,
			User:      updatedPost.User,
			CreatedAt: timestamppb.New(updatedPost.CreateAt),
			UpdatedAt: timestamppb.New(updatedPost.UpdatedAt),
		},
	}
	return res, nil
}


GetPost gRPC Service Handler

Here, we extract the post ID from the request and call the FindPostById() service to retrieve that document from the collection.

gapi/rpc_get_post.go


package gapi

import (
	"context"
	"strings"

	"github.com/wpcodevo/golang-mongodb/pb"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/timestamppb"
)

func (postServer *PostServer) GetPost(ctx context.Context, req *pb.PostRequest) (*pb.PostResponse, error) {
	postId := req.GetId()

	post, err := postServer.postService.FindPostById(postId)
	if err != nil {
		if strings.Contains(err.Error(), "Id exists") {
			return nil, status.Errorf(codes.NotFound, err.Error())

		}
		return nil, status.Errorf(codes.Internal, err.Error())
	}

	res := &pb.PostResponse{
		Post: &pb.Post{
			Id:        post.Id.Hex(),
			Title:     post.Title,
			Content:   post.Content,
			Image:     post.Image,
			User:      post.User,
			CreatedAt: timestamppb.New(post.CreateAt),
			UpdatedAt: timestamppb.New(post.UpdatedAt),
		},
	}
	return res, nil
}


DeletePost gRPC Service Handler

Now that we are able to create, update and read a post, we should be able to delete one from the database. For the DeletePost handler, we’ll call the DeletePost() service and provide it with the ID of the post to be removed from the database.

If the document has been removed from the database then we return a boolean of true else an error.

gapi/rpc_delete_post.go


package gapi

import (
	"context"
	"strings"

	"github.com/wpcodevo/golang-mongodb/pb"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

func (postServer *PostServer) DeletePost(ctx context.Context, req *pb.PostRequest) (*pb.DeletePostResponse, error) {
	postId := req.GetId()

	if err := postServer.postService.DeletePost(postId); err != nil {
		if strings.Contains(err.Error(), "Id exists") {
			return nil, status.Errorf(codes.NotFound, err.Error())
		}
		return nil, status.Errorf(codes.Internal, err.Error())
	}

	res := &pb.DeletePostResponse{
		Success: true,
	}

	return res, nil
}


GetPosts gRPC Service Handler

The last method to implement is GetPosts which is slightly different. The previous operations were unary which means the client sends a single request and the server returns a single response.

We’ll use server streaming to send the posts to the client.

The server will keep on streaming the post messages until all the posts have been sent to the client.

To retrieve the posts from the MongoDB database, we first need to extract the page number and the limit from the request before calling the FindPosts() service.

gapi/rpc_list_posts.go


package gapi

import (
	"github.com/wpcodevo/golang-mongodb/pb"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/timestamppb"
)

func (postServer *PostServer) GetPosts(req *pb.GetPostsRequest, stream pb.PostService_GetPostsServer) error {
	var page = req.GetPage()
	var limit = req.GetLimit()

	posts, err := postServer.postService.FindPosts(int(page), int(limit))
	if err != nil {
		return status.Errorf(codes.Internal, err.Error())
	}

	for _, post := range posts {
		stream.Send(&pb.Post{
			Id:        post.Id.Hex(),
			Title:     post.Title,
			Content:   post.Content,
			Image:     post.Image,
			CreatedAt: timestamppb.New(post.CreateAt),
			UpdatedAt: timestamppb.New(post.UpdatedAt),
		})
	}

	return nil
}


Testing the gRPC Services with Evans Cli

-Create a post

golang evans create post rpc

-Get a single post

golang evans get post

-Get the first 10 posts

golang evans get posts rpc

-Update a post

golang evans update post rpc

-Delete a post

golang evans delete post rpc

Create the gRPC Clients

In one of the previous tutorials, Build Golang gRPC Server and Client: Access & Refresh Tokens, we created the gRPC clients to register a new user and sign in as the registered user.

The CRUD gRPC clients will follow the same format.

gRPC Client to Create a Post

With the client folder, create a createPost_client.go file and add the code snippets below.

client/createPost_client.go


package client

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/wpcodevo/golang-mongodb/pb"
	"google.golang.org/grpc"
)

type CreatePostClient struct {
	service pb.PostServiceClient
}

func NewCreatePostClient(conn *grpc.ClientConn) *CreatePostClient {
	service := pb.NewPostServiceClient(conn)

	return &CreatePostClient{service}
}

func (createPostClient *CreatePostClient) CreatePost(args *pb.CreatePostRequest) {

	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(time.Millisecond*1000))
	defer cancel()

	res, err := createPostClient.service.CreatePost(ctx, args)

	if err != nil {
		log.Fatalf("CreatePost: %v", err)
	}

	fmt.Println(res)
}


gRPC Client to Update a Post

Next, add the following code to update a post in the database.

client/updatePost_client.go


package client

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/wpcodevo/golang-mongodb/pb"
	"google.golang.org/grpc"
)

type UpdatePostClient struct {
	service pb.PostServiceClient
}

func NewUpdatePostClient(conn *grpc.ClientConn) *UpdatePostClient {
	service := pb.NewPostServiceClient(conn)

	return &UpdatePostClient{service}
}

func (updatePostClient *UpdatePostClient) UpdatePost(args *pb.UpdatePostRequest) {

	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(time.Millisecond*5000))
	defer cancel()

	res, err := updatePostClient.service.UpdatePost(ctx, args)

	if err != nil {
		log.Fatalf("UpdatePost: %v", err)
	}

	fmt.Println(res)
}


gRPC Client to Get a Single Post

client/getPost_client.go


package client

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/wpcodevo/golang-mongodb/pb"
	"google.golang.org/grpc"
)

type GetPostClient struct {
	service pb.PostServiceClient
}

func NewGetPostClient(conn *grpc.ClientConn) *GetPostClient {
	service := pb.NewPostServiceClient(conn)

	return &GetPostClient{service}
}

func (getPostClient *GetPostClient) GetPost(args *pb.PostRequest) {

	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(time.Millisecond*5000))
	defer cancel()

	res, err := getPostClient.service.GetPost(ctx, args)

	if err != nil {
		log.Fatalf("GetPost: %v", err)
	}

	fmt.Println(res)
}


gRPC Client to Get All Posts

client/listPosts_client.go


package client

import (
	"context"
	"fmt"
	"io"
	"log"
	"time"

	"github.com/wpcodevo/golang-mongodb/pb"
	"google.golang.org/grpc"
)

type ListPostsClient struct {
	service pb.PostServiceClient
}

func NewListPostsClient(conn *grpc.ClientConn) *ListPostsClient {
	service := pb.NewPostServiceClient(conn)

	return &ListPostsClient{service}
}

func (listPostsClient *ListPostsClient) ListPosts(args *pb.GetPostsRequest) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(time.Millisecond*5000))
	defer cancel()

	stream, err := listPostsClient.service.GetPosts(ctx, args)
	if err != nil {
		log.Fatalf("ListPosts: %v", err)
	}

	for {
		res, err := stream.Recv()

		if err == io.EOF {
			break
		}

		if err != nil {
			log.Fatalf("ListPosts: %v", err)
		}

		fmt.Println(res)
	}

}


gRPC Client to Delete a Post

client/deletePost_client.go


package client

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/wpcodevo/golang-mongodb/pb"
	"google.golang.org/grpc"
)

type DeletePostClient struct {
	service pb.PostServiceClient
}

func NewDeletePostClient(conn *grpc.ClientConn) *DeletePostClient {
	service := pb.NewPostServiceClient(conn)

	return &DeletePostClient{service}
}

func (deletePostClient *DeletePostClient) DeletePost(args *pb.PostRequest) {

	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(time.Millisecond*5000))
	defer cancel()

	_, err := deletePostClient.service.DeletePost(ctx, args)

	if err != nil {
		log.Fatalf("DeletePost: %v", err)
	}

	fmt.Println("Post deleted successfully")
}


Update the Client Main File

Now, update the cmd/client/main.go file with the code snippets below.

cmd/client/main.go


package main

import (
	"log"

	"github.com/wpcodevo/golang-mongodb/client"
	"github.com/wpcodevo/golang-mongodb/pb"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

const (
	address = "0.0.0.0:8080"
)

func main() {
	conn, err := grpc.Dial(address, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock())

	if err != nil {
		log.Fatalf("failed to connect: %v", err)
	}

	defer conn.Close()

	// Sign Up
	if false {
		signUpUserClient := client.NewSignUpUserClient(conn)
		newUser := &pb.SignUpUserInput{
			Name:            "Jane Smith",
			Email:           "janesmith@gmail.com",
			Password:        "password123",
			PasswordConfirm: "password123",
		}
		signUpUserClient.SignUpUser(newUser)
	}

	// Sign In
	if false {
		signInUserClient := client.NewSignInUserClient(conn)

		credentials := &pb.SignInUserInput{
			Email:    "janesmith@gmail.com",
			Password: "password123",
		}
		signInUserClient.SignInUser(credentials)
	}

	// Get Me
	if false {

		getMeClient := client.NewGetMeClient(conn)
		id := &pb.GetMeRequest{
			Id: "628cffb91e50302d360c1a2c",
		}
		getMeClient.GetMeUser(id)

	}

	// List Posts
	if false {
		listPostsClient := client.NewListPostsClient(conn)

		var page int64 = 1
		var limit int64 = 10
		args := &pb.GetPostsRequest{
			Page:  &page,
			Limit: &limit,
		}

		listPostsClient.ListPosts(args)
	}

	// Create Post
	if false {
		createPostClient := client.NewCreatePostClient(conn)

		args := &pb.CreatePostRequest{
			Title:   "My second gRPC post with joy",
			Content: "It's always good to learn new technologies",
			User:    "62908e0a42a608d5aeae2f64",
			Image:   "default.png",
		}

		createPostClient.CreatePost(args)
	}

	// Update Post
		if false {
		updatePostClient := client.NewUpdatePostClient(conn)

		title := "My new updated title for my blog"
		args := &pb.UpdatePostRequest{
			Id:    "629169e00a6c7cfd24e2129d",
			Title: &title,
		}

		updatePostClient.UpdatePost(args)
	}

	// Get Post
	if true {
		getPostClient := client.NewGetPostClient(conn)

		args := &pb.PostRequest{
			Id: "629169e00a6c7cfd24e2129d",
		}

		getPostClient.GetPost(args)
	}

	// Delete Post
	if false {
		deletePostClient := client.NewDeletePostClient(conn)

		args := &pb.PostRequest{
			Id: "629147ff3c92aed11d49394b",
		}

		deletePostClient.DeletePost(args)
	}
}


Depending on the service you want to call on the server, you can change the condition of the if statement to true .

Conclusion

Congrats for reaching the end. In this article, you learned how to build a CRUD gRPC API server with Golang, MongoDB-Go-driver, and Docker-compose. You also learned how to build a gRPC client to interact with the gRPC API.

Check out the source code on GitHub