In this article, you’ll learn how to create a gRPC server to register a user and verify their email address using Golang, MongoDB-Go-driver, Gomail, 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 Golang gRPC Server and Client SignUp User & Verify Email

What the course will cover

  • How to create a gRPC API method to register a user and send a verification code to their email address.
  • How to create a gRPC API to verify the user’s email.
  • How to interact with the gRPC API server with Golang Evans.
  • How to create a gRPC client to register a new user.

Prerequisites

Software

VS Code Extensions

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

gRPC Project setup in Golang

Visit the official gRPC documentation to download the latest Protocol Buffer plugins for Golang.

Create the gRPC Request and Response Messages

To start using gRPC, we first need to create a .proto file describing the service and methods we want to use remotely using the protocol buffer language.

A common pattern is to keep the .proto files in a separate folder. This will make your project cleaner and help you generate the server and client interfaces or stubs easily.

In the root directory of the project, create a proto folder to contain all the .proto files.

Define the gRPC User messages

First, we start by specifying the protocol buffer language which in this case is proto3.

We also need to specify the package name using the package keyword. We’ll later refer to it when setting up gRPC with Golang.

Lastly, we use the option keyword to inform the Protobuf compiler where we want to put the generated stubs. You can use multiple option statements depending on the language you want to generate the stubs for.

To define the go_package option statement, you need to add the package name to the end of your Golang module name.

Also, to define the field of a protocol buffer message, we specify the field type followed by the field name and a unique number that will be used by the gRPC framework to identify the fields in the message binary format.

The User message list all the fields a user object will hold. Also, the created_at and updated_at fields have a timestamp type.

Timestamp is not a built-in type of protobuf but is a well-known data type and Google has already added it to their standard library.

You can refer to it as google.protobuf.Timestamp and remember to also import it from Google’s standard library.

proto/user.proto


syntax = "proto3";

package pb;

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

message User {
  string id = 1;
  string name = 2;
  string email = 3;
  enum role {
    user = 0;
    admin = 1;
  }
  google.protobuf.Timestamp created_at = 4;
  google.protobuf.Timestamp updated_at = 5;
}

message GenericResponse {
  string status = 1;
  string message = 2;
}

Define the gRPC Request and Response Message to SignUp User

Within the proto folder, create an rpc_signup_user.proto file to contain the request and response messages needed to create a user.

The SignUpUserInput message is the object containing the fields required to create a new user from the client.

It must provide four required fields – the user’s name, email, password, and password confirm fields.

Next, let’s create the SignUpUserResponse message to contain the information of the created user.

The SignUpUserResponse message will contain a field with a custom type User and a unique number of 1. The User type is the message we defined in the user.proto file so you need to import it.

proto/rpc_signup_user.proto


syntax = "proto3";

package pb;

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

import "user.proto";

message SignUpUserInput {
  string name = 1;
  string email = 2;
  string password = 3;
  string passwordConfirm = 4;
}

message SignUpUserResponse { User user = 1; }


If you are using the VS Code Proto3 extension you will see a red line under the import since by default protoc will look for the file in the root directory but the proto files are actually in the proto folder.

To fix that we need to tell Protoc where the proto files are located. In VS Code, open the settings page then search for Proto3 and click on the Edit in settings.json link.

Next, add this option to the Protoc configuration.


{
   "options": ["--proto_path=proto"]
}

Create the gRPC Service Methods

Since we have created the request and response data structures of the gRPC API, it’s now time to define the gRPC service and add the RPC definitions.

In the auth_service.proto file, let’s define a service called AuthService instead of a message.

The AuthService will contain two RPCs called SignUpUser and VerifyEmail .

The SignUpUser gRPC API will be responsible for creating a new user and sending an email verification code to the user’s email.

Also, the VerifyEmail gRPC API will be responsible for verifying the user’s email.

proto/auth_service.proto


syntax = "proto3";

package pb;

import "rpc_signup_user.proto";
import "user.proto";

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

service AuthService {
  rpc SignUpUser(SignUpUserInput) returns (GenericResponse) {}
  rpc VerifyEmail(VerifyEmailRequest) returns (GenericResponse) {}
}

message VerifyEmailRequest { string verificationCode = 1; }

In the above, we have implemented a Unary gRPC which is the simplest gRPC since the client sends a single request and gets a single response from the server.

Generate the gRPC client and server interfaces

We are done creating the gRPC APIs for registering and verifying the user. It’s now time to generate the Golang code from the service definitions.

Run the following Protoc command to generate the Golang code into the pb folder.


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

Once the generation is successful you should see the generated files in the pb folder with a .pb.go extension.

Each file in the pb folder will correspond to a .proto file created in the proto folder.

You should see an additional file with a _grpc.pb.go extension. This is the file that contains the gRPC server and client interfaces or stubs that we’ll refer to write the real implementations later.

Also, you should see some errors with the imports since the Golang gRPC package is not installed yet.

Run the following command in the terminal to download the required packages:


go mod tidy

Start the gRPC Server

In the root directory, create a gapi folder and within the gapi folder create a file named server.go.

The server.go file will contain the gRPC server code to serve gRPC requests.

In the pb folder, open the file with the _grpc.pb.go extension and you should see an AuthServiceServer interface that our server must implement in order to become a gRPC server.

pb/auth_service_grpc.pb.go


type AuthServiceServer interface {
	SignUpUser(context.Context, *SignUpUserInput) (*GenericResponse, error)
	VerifyEmail(context.Context, *VerifyEmailRequest) (*GenericResponse, error)
	mustEmbedUnimplementedAuthServiceServer()
}

In recent versions of gRPC, apart from the server interface, Protoc also generates an unimplemented server (UnimplementedAuthServiceServer) struct where all RPC functions are already provided but they return an unimplemented error.

You need to add the unimplemented server (pb.UnimplementedAuthServiceServer) to your custom-defined server to enable forward-compatible which means the server can accept calls to the RPC methods before they are actually implemented.

gapi/server.go


package gapi

import (
	"go.mongodb.org/mongo-driver/mongo"
)

type Server struct {
	pb.UnimplementedAuthServiceServer
	config         config.Config
	authService    services.AuthService
	userService    services.UserService
	userCollection *mongo.Collection
}

func NewGrpcServer(config config.Config, authService services.AuthService,
	userService services.UserService, userCollection *mongo.Collection) (*Server, error) {

	server := &Server{
		config:         config,
		authService:    authService,
		userService:    userService,
		userCollection: userCollection,
	}

	return server, nil
}


In the main.go file, we already have the code to start the Gin Gonic server. We need to put the code to start the Gin server in a separate function.

main.go


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)
	log.Fatal(server.Run(":" + config.Port))
}

Now create a startGrpcServer function in the main.go file and add the following code to start the gRPC server.

main.go


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

	grpcServer := grpc.NewServer()
	pb.RegisterAuthServiceServer(grpcServer, server)
	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)
	}
}

Evoke the startGrpcServer() function in the main function. You can easily switch between the Gin and gRPC server by commenting one out.

Also, you need to add the gRPC server address to the .env file and update the config/default.go file to load it.

.env


GRPC_SERVER_ADDRESS=0.0.0.0:8080

That means the gRPC server will run on port 8080 whilst the Gin server runs on 8000.

The main.go should now look like this:

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
)

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)

	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) {
	server, err := gapi.NewGrpcServer(config, authService, userService, authCollection)
	if err != nil {
		log.Fatal("cannot create grpc server: ", err)
	}

	grpcServer := grpc.NewServer()
	pb.RegisterAuthServiceServer(grpcServer, server)
	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)
	log.Fatal(server.Run(":" + config.Port))
}


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


go run main.go

Test the gRPC API Server with Golang Evans

Evans is a gRPC client that allows you to send gRPC requests in an interactive console.

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

Am on Windows so I installed it with the go install command.

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

Run the command below to connect Evans to the server on port 8080.


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

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

evans console grpc server connection

Next, run call SignUpUser to evoke the RPC method on the server and provide the required fields.

The server should return the unimplemented error since the SignUpUser method is not implemented on the server yet. However, this shows that the gRPC server is working and can accept gRPC requests from the client.

evans console grpc server connection call service

Create the gRPC API Controllers

When you open the auth_service_grpc.pb.go file in the pb folder, you should see the basic gRPC methods that are required by the AuthServiceServer interface.


func (UnimplementedAuthServiceServer) SignUpUser(context.Context, *SignUpUserInput) (*GenericResponse, error) {
	return nil, status.Errorf(codes.Unimplemented, "method SignUpUser not implemented")
}
func (UnimplementedAuthServiceServer) VerifyEmail(context.Context, *VerifyEmailRequest) (*GenericResponse, error) {
	return nil, status.Errorf(codes.Unimplemented, "method VerifyEmail not implemented")
}

What we have to do is to implement them on our own server struct.

Register User gRPC Controller

Create an rpc_signup_user.go file in the gapi folder and paste the code snippets below into it.

gapi/rpc_signup_user.go


package gapi

import (
	"context"
	"strings"

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

func (server *Server) SignUpUser(ctx context.Context, req *pb.SignUpUserInput) (*pb.GenericResponse, error) {
	if req.GetPassword() != req.GetPasswordConfirm() {
		return nil, status.Errorf(codes.InvalidArgument, "passwords do not match")
	}

	user := models.SignUpInput{
		Name:            req.GetName(),
		Email:           req.GetEmail(),
		Password:        req.GetPassword(),
		PasswordConfirm: req.GetPasswordConfirm(),
	}

	newUser, err := server.authService.SignUpUser(&user)

	if err != nil {
		if strings.Contains(err.Error(), "email already exist") {
			return nil, status.Errorf(codes.AlreadyExists, "%s", err.Error())

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

	// Generate Verification Code
	code := randstr.String(20)

	verificationCode := utils.Encode(code)

	// Update User in Database
	server.userService.UpdateUserById(newUser.ID.Hex(), "verificationCode", verificationCode)

	var firstName = newUser.Name

	if strings.Contains(firstName, " ") {
		firstName = strings.Split(firstName, " ")[0]
	}

	// ? Send Email
	emailData := utils.EmailData{
		URL:       server.config.Origin + "/verifyemail/" + code,
		FirstName: firstName,
		Subject:   "Your account verification code",
	}

	err = utils.SendEmail(newUser, &emailData, "verificationCode.html")
	if err != nil {
		return nil, status.Errorf(codes.Internal, "There was an error sending email: %s", err.Error())

	}

	message := "We sent an email with a verification code to " + newUser.Email

	res := &pb.GenericResponse{
		Status:  "success",
		Message: message,
	}
	return res, nil
}


Here are some of the key things to note in the above code:

  • First, I implemented the unimplemented SignUpUser method of the UnimplementedAuthServiceServer struct in my own server struct.
  • In the SignUpUser method, I checked if the provided passwords are equal before passing the credentials to the SignUpUser() service we defined in one of the previous tutorials to add the user to the MongoDB database.
  • Next, I generated the email verification code and sent it to the user’s email.
  • Lastly, I returned a message to the client indicating that the email was sent successfully.

Verify User gRPC Controller

Next, let’s implement the VerifyEmail method to verify and update the user’s credentials in the MongoDB database.

To verify the user, I retrieved the verification code from the request body and hashed it before making a query to the MongoDB database to check if a user with that token exists.

Finally, I updated the verified field of the user document to true if that user exists and returned a success message to the client.

gapi/rpc_verify_user.go


package gapi

import (
	"context"
	"time"

	"github.com/wpcodevo/golang-mongodb/pb"
	"github.com/wpcodevo/golang-mongodb/utils"
	"go.mongodb.org/mongo-driver/bson"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

func (server *Server) VerifyEmail(ctx context.Context, req *pb.VerifyEmailRequest) (*pb.GenericResponse, error) {
	code := req.GetVerificationCode()

	verificationCode := utils.Encode(code)

	query := bson.D{{Key: "verificationCode", Value: verificationCode}}
	update := bson.D{{Key: "$set", Value: bson.D{{Key: "verified", Value: true}, {Key: "updated_at", Value: time.Now()}}}, {Key: "$unset", Value: bson.D{{Key: "verificationCode", Value: ""}}}}
	result, err := server.userCollection.UpdateOne(ctx, query, update)
	if err != nil {
		return nil, status.Errorf(codes.Internal, err.Error())
	}

	if result.MatchedCount == 0 {
		return nil, status.Errorf(codes.PermissionDenied, "Could not verify email address")
	}

	res := &pb.GenericResponse{
		Status:  "success",
		Message: "Email verified successfully",
	}
	return res, nil
}


Create the gRPC Client to Register a User

In the root directory, create a client folder and create a main.go within it.

Next, add the following code snippets to create the gRPC client. The gRPC client will connect to the server on port 8080.


package main

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

	"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()

	client := pb.NewAuthServiceClient(conn)

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

	newUser := &pb.SignUpUserInput{
		Name:            "James Smith",
		Email:           "jamesmith@gmail.com",
		Password:        "password123",
		PasswordConfirm: "password123",
	}

	res, err := client.SignUpUser(ctx, newUser)
	if err != nil {
		log.Fatalf("SignUpUser: %v", err)
	}

	fmt.Println(res)

}


Here is a summary of the code snippet above:

  • I used the grpc.Dial() method to create the connection between the server and the client.
  • Also, I passed the WithTransportCredentials option to create an insecure connection by turning off TLS since we are in a development environment.
  • Lastly, I provided the required fields to create a user and called the SignUpUser RPC method to add the new user.

Run the following command in the terminal to create a new user in the MongoDB database.


go run client/main.go

Conclusion

Congrats for reaching the end. In this article, you learned how to build a Golang gRPC server and client to register a user and verify their email address with MongoDB, Gomail, and Docker-compose.

Check out the source code on GitHub