Movatterモバイル変換


[0]ホーム

URL:


CodevoWeb

PressESC to close

Build Golang gRPC Server and Client: SignUp User & Verify Email

4Comments33

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 withGolang 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 theofficial 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 theprotocol 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 aproto 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 isproto3.

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

Lastly, we use theoption 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 thego_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.

TheUser message list all the fields a user object will hold. Also, thecreated_at andupdated_at fields have a timestamp type.

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

You can refer to it asgoogle.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 theproto folder, create anrpc_signup_user.proto file to contain the request and response messages needed to create a user.

TheSignUpUserInput 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 theSignUpUserResponse message to contain the information of the created user.

TheSignUpUserResponse message will contain a field with a custom typeUser and a unique number of1. TheUser type is the message we defined in theuser.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 theVS Code Proto3 extension you will see a red line under the import since by defaultprotoc 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 forProto3 and click on theEdit 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 theauth_service.proto file, let’s define a service calledAuthService instead of a message.

TheAuthService will contain two RPCs calledSignUpUser andVerifyEmail .

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

Also, theVerifyEmail 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 thepb 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 thepb folder with a.pb.go extension.

Each file in thepb folder will correspond to a.proto file created in theproto 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 agapi folder and within thegapi folder create a file namedserver.go.

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

In thepb folder, open the file with the_grpc.pb.go extension and you should see anAuthServiceServer 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 gapiimport ("go.mongodb.org/mongo-driver/mongo")type Server struct {pb.UnimplementedAuthServiceServerconfig         config.ConfigauthService    services.AuthServiceuserService    services.UserServiceuserCollection *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 themain.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 = trueserver.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 astartGrpcServer function in themain.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 thestartGrpcServer() function in themain 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 theconfig/default.go file to load it.

.env

GRPC_SERVER_ADDRESS=0.0.0.0:8080

That means the gRPC server will run on port8080 whilst the Gin server runs on8000.

Themain.go should now look like this:

main.go

package mainimport ("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.Enginectx         context.Contextmongoclient *mongo.Clientredisclient *redis.ClientuserService         services.UserServiceUserController      controllers.UserControllerUserRouteController routes.UserRouteControllerauthCollection      *mongo.CollectionauthService         services.AuthServiceAuthController      controllers.AuthControllerAuthRouteController routes.AuthRouteController)func init() {config, err := config.LoadConfig(".")if err != nil {log.Fatal("Could not load environment variables", err)}ctx = context.TODO()// Connect to MongoDBmongoconn := 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 Redisredisclient = 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...")// CollectionsauthCollection = 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 = trueserver.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 theEvans GitHub page to download and install the binary for Mac, Linux, or windows.

Am on Windows so I installed it with thego 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, runshow service to list all services and RPCs available on the server.

evans console grpc server connection

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

The server should return the unimplemented error since theSignUpUser 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 theauth_service_grpc.pb.go file in thepb folder, you should see the basic gRPC methods that are required by theAuthServiceServer 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 anrpc_signup_user.go file in thegapi folder and paste the code snippets below into it.

gapi/rpc_signup_user.go

package gapiimport ("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 Codecode := randstr.String(20)verificationCode := utils.Encode(code)// Update User in Databaseserver.userService.UpdateUserById(newUser.ID.Hex(), "verificationCode", verificationCode)var firstName = newUser.Nameif strings.Contains(firstName, " ") {firstName = strings.Split(firstName, " ")[0]}// ? Send EmailemailData := 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.Emailres := &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 unimplementedSignUpUser method of theUnimplementedAuthServiceServer struct in my own server struct.
  • In theSignUpUser method, I checked if the provided passwords are equal before passing the credentials to theSignUpUser() 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 theVerifyEmail 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 totrue if that user exists and returned a success message to the client.

gapi/rpc_verify_user.go

package gapiimport ("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 amain.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 mainimport ("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 thegrpc.Dial() method to create the connection between the server and the client.
  • Also, I passed theWithTransportCredentials option to create an insecure connection by turning offTLS since we are in a development environment.
  • Lastly, I provided the required fields to create a user and called theSignUpUser 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 thesource code on GitHub

Share Article:

API with Golang, Gin Gonic & MongoDB: Forget/Reset Password

Left Arrow

Build Golang gRPC Server and Client: Access & Refresh Tokens

Right Arrow

4 Comments

  1. Foreston February 26, 2023
    Reply
    • Edemon March 1, 2023

      Thank you for the suggestion, I will definitely check out Warthog.

      Reply
  2. Vishnuon October 3, 2023

    Your blogs are very informative

    Reply

Leave a ReplyCancel reply

This site is protected by reCAPTCHA and the GooglePrivacy Policy andTerms of Service apply.

This site uses Akismet to reduce spam.Learn how your comment data is processed.

Support Me!

paypal donate button

Recent posts

Categories


[8]ページ先頭

©2009-2025 Movatter.jp