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:
- API with Golang + MongoDB + Redis + Gin Gonic: Project Setup
- Golang & MongoDB: JWT Authentication and Authorization
- API with Golang + MongoDB: Send HTML Emails with Gomail
- API with Golang, Gin Gonic & MongoDB: Forget/Reset Password
- Build Golang gRPC Server and Client: SignUp User & Verify Email
- Build Golang gRPC Server and Client: Access & Refresh Tokens
- Build CRUD RESTful API Server with Golang, Gin, 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
- Golang – to build the gRPC server and client
- Protocol Buffer Compiler – To compile the
.proto
files
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.
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.
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:
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
-Get a single post
-Get the first 10 posts
-Update a post
-Delete a post
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