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:
- 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
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
- Golang – to build the gRPC server and client
- Protocol Buffer Compiler – To compile the
.proto
files
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.
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.
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
A Nice gRPC GUI client https://github.com/Forest33/warthog
Thank you for the suggestion, I will definitely check out Warthog.
Your blogs are very informative
Thanks