Part 1 of 3GO: API Dialing GRPC Endpoints –Setting Up The Development Container And Building The Services

The purpose of this article is to demonstrate how to setup a development environment that is containerized and has multiple microservices in a mono-repo that is all debugable with breakpoints at each point of the call chain from the HTTP Request, to the GRPC Request, the GRPC Response, and the HTTP Response.

Overview

In this overview we will be creating 3 microservices:

  • An HTTP API
  • A Strings Service
  • A Numbers Service

We will be defining the Strings and Numbers service through Protobuff files and then generate server code. The HTTP API will implement the necessary client side code to communicate with these GRPC services.

We want to setup the Strings Service to convert an input string to a fully Upper-Case or fully Lower-Case string.

We want to setup the Numbers service to accept two numbers and then Add them or Subtract them.

We want the API to have the following HTTP endpoints and then communicate with the Strings or Numbers service as appropriate over GRPC and then return the GRPC response as the HTTP Response body:

  • api/StringsService/MakeUpperCase
  • api/StringsService/MakeLowerCase
  • api/NumbersService/AddTwoNumbers
  • api/NumbersService/SubtractTwoNumbers

We also want to be able to debug the entire operation from the HTTP Request, to the GRPC Request, to the Service Operations, to the GRPC Response, and the final HTTP Response.

Show me the code: https://github.com/woodman231/api_dialing_grpc

Part 1: Setting Up The Development Container And Building The Services
Part 2: Running Each Service In Its Own Docker Container — Link
Part 3: Debug Each Service While It Is Running In Its Own Docker Container — Link

Part 1 – Setting Up The Development Container And Building The Services

To get started let’s create a development container in visual studio.

1 – Create a new folder on your computer. I will call mine “api_dialing_grpc”, and then open the folder in Visual Studio Code

2 – Do a CTRL+SHIFT+P to open the command pallet in Visual Studio Code and select the “Dev Containers: “Add Dev Container Configuration Files…” option

3 – Select the option to view all configurations

4 – Choose the “Go” configuration

5 – Select the “1.18” version

6 – Select the “lts/*” Node.js version

7 – Check the boxes for Docker (Moby) support (Docker-in-Docker) and GitHub CLI

8 – And finally, select the latest Moby version when prompted

This should give your /.devcontainer/devcontainer.json file to have the following code

// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/go
{
    "name": "Go",
    "build": {
        "dockerfile": "Dockerfile",
        "args": {
            // Update the VARIANT arg to pick a version of Go: 1, 1.19, 1.18
            // Append -bullseye or -buster to pin to an OS version.
            // Use -bullseye variants on local arm64/Apple Silicon.
            "VARIANT": "1.18",
            // Options
            "NODE_VERSION": "lts/*"
        }
    },
    "runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" ],

    // Configure tool-specific properties.
    "customizations": {
        // Configure properties specific to VS Code.
        "vscode": {
            // Set *default* container specific settings.json values on container create.
            "settings": { 
                "go.toolsManagement.checkForUpdates": "local",
                "go.useLanguageServer": true,
                "go.gopath": "/go"
            },
            
            // Add the IDs of extensions you want installed when the container is created.
            "extensions": [
                "golang.Go"
            ]
        }
    },

    // Use 'forwardPorts' to make a list of ports inside the container available locally.
    // "forwardPorts": [],

    // Use 'postCreateCommand' to run commands after the container is created.
    // "postCreateCommand": "go version",

    // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
    "remoteUser": "vscode",
    "features": {
        "docker-in-docker": "latest",
        "github-cli": "latest"
    }
}
Furthermore, your /.devcontainer/Dockerfile should have the following code
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/go/.devcontainer/base.Dockerfile

# [Choice] Go version (use -bullseye variants on local arm64/Apple Silicon): 1, 1.19, 1.18, 1-bullseye, 1.19-bullseye, 1.18-bullseye, 1-buster, 1.19-buster, 1.18-buster
ARG VARIANT="1.19-bullseye"
FROM mcr.microsoft.com/vscode/devcontainers/go:0-${VARIANT}

# [Choice] Node.js version: none, lts/*, 18, 16, 14
ARG NODE_VERSION="none"
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi

# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
#     && apt-get -y install --no-install-recommends <your-package-list-here>

# [Optional] Uncomment the next lines to use go get to install anything else you need
# USER vscode
# RUN go get -x <your-dependency-or-tool>

# [Optional] Uncomment this line to install global node packages.
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1
When the box comes up click the box to “Reopen in Container”
When the container is built it is time to create a workspace for our Go project. The dev containers certainly seem to use go workspaces allot better then relying on the GOROOT environment variable.

Create a GO Workspace

Do a CTRL+SHIFT+` to bring up the command prompt. Execute the following command:

go work init
This will create a go.work file
Create a new folder called “protos”
mkdir protos
Initialize a module that will represent where on github this would be hosted for you. For me it is github.com/woodman231/api_dialing_grpc/protos. Execute the following command (you can change this to your github username if you prefer).
go mod init github.com/woodman231/api_dialing_grpc/protos
Add this protos module to your workspace

Assuming that your command prompt is at /workspaces/api_dialing_grpc/protos, the commands will be:

cd ..
go work use ./protos
This should modify your go.work file to have the following code:
go 1.18

use ./protos
Also, your /protos/go.mod file should have the following code
module github.com/woodman231/api_dialing_grpc/protos

go 1.18

Create PROTO Files for the Microservices

We are now ready to define the shape of our microservices.

In the “protos” folder create a new file called “string_service.proto”. Give it the following code:

syntax = "proto3";
package string_services;

option go_package = "github.com/woodman231/api_dialing_grpc/protos/stringspb";

message OperationRequest {
    string input_string = 1;
}

message OperationResult {
    string output_string = 1;
}

service StringService {
    rpc MakeUpperCase(OperationRequest) returns (OperationResult);
    rpc MakeLowerCase(OperationRequest) returns (OperationResult);
}
In the “protos” folder create a new file called “number_services.proto”. Give it the following code:
syntax = "proto3";
package number_services;

option go_package = "github.com/woodman231/api_dialing_grpc/protos/numberspb";

message OperationRequest {
    int32 input_number_one = 1;
    int32 input_number_two = 2;
}

message OperationResult {
    int32 output_number = 1;
}

service NumberService {
    rpc AddTwoNumbers(OperationRequest) returns (OperationResult);
    rpc SubtractTwoNumbers(OperationRequest) returns (OperationResult);
}
Again, you can use your github username in place of woodman231.

Let’s go over these files. The string_service.proto file added an extra option to tell the proto compiler that the go_package for this will be github.com/woodman231/api_dialing_grpc/protos/stringspb. It then defines a message for an OperationRequest which will serve as the standard input model for our functions. It also creates an OperationResult message that will serve as the standard model for the output of our functions. Finally we define a service with two rpc functions: MakeUpperCase that will take in a parameter of OperationRequest and return an OperationResult; and a MakeLowerCase function that will take in an OperationRequest and return an OperationResult.

The number_service.proto file is very similar. We are telling the proto compiler what go package we expect to run. We have an OperationRequest that will serve as the standard message for the input parameter of our functions and an OperationResult message that will serve as the standard return of our functions. This time we are defining two numbers as properties for our OperationRequest. The service is then defined with two functions AddTwoNumbers and SubtractTwoNumbers. Again, each of them takes in an OperationRequest message and returns an OperationResult message.

Now that we have defined what our microservices are going to look like it is time to compile them to GO files that our Server code can use.

Before we can do that, we must install the proto compiler.

Install the PROTO Compiler into your Development Container

We will also need the proto compile plugins for go and gRPC. Update lines 11 through 18 in your /.devcontainer/Dockerfile to have the following code

# [Optional] Uncomment this section to install additional OS packages.
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
    && apt-get -y install --no-install-recommends protobuf-compiler

# [Optional] Uncomment the next lines to use go get to install anything else you need
USER vscode
RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
RUN go install google.golang.org/grpc/cmd/protoc-gen-go-gRPC@v1.2
Your entire Dockerfile should look like this now:
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/go/.devcontainer/base.Dockerfile

# [Choice] Go version (use -bullseye variants on local arm64/Apple Silicon): 1, 1.19, 1.18, 1-bullseye, 1.19-bullseye, 1.18-bullseye, 1-buster, 1.19-buster, 1.18-buster
ARG VARIANT="1.19-bullseye"
FROM mcr.microsoft.com/vscode/devcontainers/go:0-${VARIANT}

# [Choice] Node.js version: none, lts/*, 18, 16, 14
ARG NODE_VERSION="none"
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi

# [Optional] Uncomment this section to install additional OS packages.
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
    && apt-get -y install --no-install-recommends protobuf-compiler

# [Optional] Uncomment the next lines to use go get to install anything else you need
USER vscode
RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
RUN go install google.golang.org/grpc/cmd/protoc-gen-go-gRPC@v1.2

# [Optional] Uncomment this line to install global node packages.
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1
After you have saved the file do a CTRL+SHIFT+P to bring up the Visual Studio Code Command Pallet and select the option for “Dev Containers: Rebuild Container”.

After your dev container has been rebuilt just type “protoc” in the terminal to confirm that the installation completed successfully.

Compile the PROTO Files to Files that GO understands

In the terminal change directories such that you are in the “/workspaces/api_dialing_grpc/protos” folder

Execute the following command in that folder

mkdir stringspb
protoc --go_out=./stringspb/ --go_opt=paths=source_relative --go-gRPC_out=./stringspb/ --go-gRPC_opt=paths=source_relative ./string_service.proto
We first created a stringspb folder for the files to land in.
We then executed the proto compiler and provided parameters to it

  • go_out tells the proto compiler to output the service client side code to ./stringspb/ folder
  • go_opt tells the proto compiler that any imported (which there aren’t any in this case) proto files are in the same folder as the proto file that we are compiling
  • go_gRPC_out tells the proto compiler to output the server side code to ./stringspb/ folder
  • go_gRPC_opt tells the proto compiler that any import (which there aren’t any in thise case) proto files are in the same folder as the proto file that we are compiling

This will create two new files

  • /workspaces/api_dialing_grpc/protos/stringspb/string_service_grpc.pb.go
  • /workspaces/api_dialing_grpc/protos/stringspb/string_service.pb.go

When you first open these files in Visual Studio code it will complain about being unable to use dependencies.

In a terminal that is at /workspaces/api_dialing_grpc/protos execute the following command

go get -d ./…
This should download the dependencies, and then after the dependencies have been downloaded restart the language server by doing a CTRL+SHIFT+P to bring up the command pallet and select the “Go: Restart Language Server” option.

At this point things should start looking normal.

Let’s go ahead and create the proto files for our numbers_service as well and then continue.

Assuming that your command prompt is still at “/workspaces/api_dialing_grpc/protos” execute the following commands:

mkdir numberspb
protoc --go_out=./numberspb/ --go_opt=paths=source_relative --go-gRPC_out=./numberspb/ --go-gRPC_opt=paths=source_relative ./number_service.proto
This will create two new files:

  • /workspaces/api_dialing_grpc/protos/numberspb/number_service_grpc.pb.go
  • /workspaces/api_dialing_grpc/protos/numberspb/number_service.pb.go


Since you installed the dependencies earlier. Things should look just fine in your Visual Studio Code.

Create Build Tasks to compile the PROTO Files in the future

The commands to compile the proto buff files are long. Thankfully Visual Studio Code allows us to create Build Tasks. In the .vscode folder add a new file called tasks.json. Give it the following code

{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "Compile Strings Service Protobuff",
            "type": "shell",
            "command": [
                "protoc",
                "--go_out=./stringspb/",
                "--go_opt=paths=source_relative",
                "--go-gRPC_out=./stringspb/",
                "--go-gRPC_opt=paths=source_relative",
                "string_service.proto"
            ],
            "options": {
                "cwd": "${workspaceFolder}/protos"
            }
        },
        {
            "label": "Compile Numbers Service Protobuff",
            "type": "shell",
            "command": [
                "protoc",
                "--go_out=./numberspb/",
                "--go_opt=paths=source_relative",
                "--go-gRPC_out=./numberspb/",
                "--go-gRPC_opt=paths=source_relative",
                "./number_service.proto"
            ],
            "options": {
                "cwd": "${workspaceFolder}/protos"
            }
        }
    ]
}
At this point you should be able to delete the following files

  • /protos/numberspb/number_service_grpc.pb.go
  • /protos/numberspb/number_service.pb.go
  • /protos/stringspb/string_service_grpc.pb.go
  • /protos/stringspb/string_service.pb.go


Then do a CTRL+SHIFT+P to bring up a command pallet and then select the option to “Tasks: Run Task” and then select the option to do the “Compile Strings Service Protobuff”, and then do it again to select the “Compile Numbers Service Protobuff” and the files should re-appear.

Build the Strings Service

In a command prompt at “/workspaces/api_dialing_grpc”, execute the following commands to create a new strings_service module and add it to the go workspace

mkdir strings_service
cd strings_service
go mod init github.com/woodman231/api_dialing_grpc/strings_service
cd ..
go work use ./strings_service
(Use your own github username if you like)

The go.work file should now look like this

go 1.18

use (
    ./protos
    ./strings_service
)
Update the code for “/strings_service/go.mod” to the following
module github.com/woodman231/api_dialing_grpc/strings_service

go 1.18

replace github.com/woodman231/api_dialing_grpc/protos => ../protos
This will make it so that it can use the protos package locally instead of trying to download it over the internet.

Create a “main.go” file in the “strings_service” folder. Give it the following code for now:

package main

import (
    "flag"
    "log"

    pb "github.com/woodman231/api_dialing_grpc/protos/stringspb"
)

var (
    port = flag.Int("port", 50051, "The server port")
)

// server is used to implment StringsServiceServer
type server struct {
    pb.UnimplementedStringServiceServer
}

func main() {
    flag.Parse()

    log.Printf("Will listen at %v", *port)
}
What we are doing here is importing a few standard packages for flag and log, and then importing the stringspb package as pb.

We then set up a struct to implement the StringsServiceServer. We use a variable that will come in via a flag “–port” when launching this program to tell the service what port to listen to requests on. And for now we just say what port we will be listening on.

If you change directories in a terminal to /workspaces/api_dialing_grpc/strings_service and do a go run main.go you will see that it says what port it will listen at.

To determine what we need to have in our file we need to read the /protos/stringspb/string_service_grpc.pb.go file. You will see that there is a portion of the file that looks like this:

This tells us that we need to create a MakeUpperCase and MakeLowerCase function that takes in two parameters a Context and an OperationRequest. It will need to return an OperationResult and an error.

In our current version of go.main we imported this package as pb and so these will need to be defined with that.

Starting at line 19 of main.go add the following code:

// MakeUpperCase implments StringsServiceServer.MakeUpperCase
func (s *server) MakeUpperCase(context.Context, *pb.OperationRequest) (*pb.OperationResult, error) {

}
Hover over context and select the quick fix to include the context import.

What we did here is add a MakeUpperCase function to our server struct that has the same signature defined in the interface of the protobuff file. Our signature includes the extra *pb… portion because we imported the package as pb. Complete this function by giving it the following code:

// MakeUpperCase implments StringsServiceServer.MakeUpperCase
func (s *server) MakeUpperCase(ctx context.Context, in *pb.OperationRequest) (*pb.OperationResult, error) {
    requestedString := in.GetInputString()

    log.Printf("Received: MakeUpperCase request for %v", requestedString)

    return &pb.OperationResult{
        OutputString: strings.ToUpper(requestedString),
    }, nil
}
Hover over the strings word and use the quick fix to include the import

What we are doing in this function is setting the results of in.GetInputString to a variable. Logging the requestedString Variable, then setting the OutputString field of the OperationResult to an upper case version of the requested string and returning that object, and a nil error message.

Now add the MakeLowerCase function by adding the following code the main.go file:

// MakeLowerCase implements StringsSErviceServer.MakeLowerCase
func (s *server) MakeLowerCase(ctx context.Context, in *pb.OperationRequest) (*pb.OperationResult, error) {
    requestedString := in.GetInputString()

    log.Printf("Received: MakeLowerCase request for %v", requestedString)

    return &pb.OperationResult{
        OutputString: strings.ToLower(requestedString),
    }, nil
}
And update the main function code as follows:
func main() {
    flag.Parse()

    lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }

    s := gRPC.NewServer()
    pb.RegisterStringServiceServer(s, &server{})
    log.Printf("server listening at %v", lis.Addr())
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}
This updated code now registers the server and starts listening for requests at the address specified.

Your entire main.go file should look like this:

package main

import (
    "context"
    "flag"
    "fmt"
    "log"
    "net"
    "strings"

    "google.golang.org/grpc"

    pb "github.com/woodman231/api_dialing_grpc/protos/stringspb"
)

var (
    port = flag.Int("port", 50051, "The server port")
)

// server is used to implment StringsServiceServer
type server struct {
    pb.UnimplementedStringServiceServer
}

// MakeUpperCase implments StringsServiceServer.MakeUpperCase
func (s *server) MakeUpperCase(ctx context.Context, in *pb.OperationRequest) (*pb.OperationResult, error) {
    requestedString := in.GetInputString()

    log.Printf("Received: MakeUpperCase request for %v", requestedString)

    return &pb.OperationResult{
        OutputString: strings.ToUpper(requestedString),
    }, nil
}

// MakeLowerCase implements StringsServiceServer.MakeLowerCase
func (s *server) MakeLowerCase(ctx context.Context, in *pb.OperationRequest) (*pb.OperationResult, error) {
    requestedString := in.GetInputString()

    log.Printf("Received: MakeLowerCase request for %v", requestedString)

    return &pb.OperationResult{
        OutputString: strings.ToLower(requestedString),
    }, nil
}

func main() {
    flag.Parse()

    lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }

    s := gRPC.NewServer()
    pb.RegisterStringServiceServer(s, &server{})
    log.Printf("server listening at %v", lis.Addr())
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

Test the Strings service

There are a couple of tools out there to test GRPC requests. I will use Postman in this demonstration.

In Postman create a new workspace called “API Dialing GRPC” (or anything else that you would like to call it) and make it a personal Workspace.

After that is done create a new API.
Change the name to strings_service
Click on the “Create Definition” button

Click on the “Import Definition” button

Click on the “Choose Files” button

Browse on your computer to where you have the string_service.proto file

Click the Import button
You should basically see the text from that file.

Click on “Collections” on the left

Click on the “New” button and not the “Create Collection” button

Select the gRPC Request button.
In the server url type in gRPC://localhost:50051

In the method drop down select the “strings_service” api, and then the MakeUpperCase method.

Click on the “Generate Example Message” button

Your screen should look something like this.

Before we click on “Invoke” we need to get the service running. In Visual Studio Code with the command prompt in the Development Container at /workspaces/api_dialing_grpc/strings_service type the following command
go mod tidy
go run main.go
You should get a notice that it is listening on port 50051.

Return to Postman and click on Invoke.

Your Postman should now look like this:

And your visual studio code terminal should look like this:
Congratulations! You now have a microservice that you can communicate with over GRPC.

Let’s save this Request in Postman to our strings_service collection by clicking on the “Save” button in Postman, and then clicking on “Create Collection” name the collection “strings_service”, and name the Request “MakeUpperCase”

Let’s now add a request to the MakeLowerCase method

In the area where the strings_service collection is hover over it and click the three dots and select Add Request and the gRPC Request option.

Enter in the gRPC://localhost:50051 URL and this time select the StringsService / MakeLowerCase method. Click on the Generate Example Message but change the input_string to have a mixture of upper and lower case letters or all upper case letters (since it will generate an all lower case one for you automatically) and invoke it. Your screen should look something like this now.
Save the request as MakeLowerCase

Not bad. Now let’s go ahead and turn off the Strings service and make the Numbers Service. In the Visual Studio Code Terminal press CTRL+C to stop the strings_service application.

Build the Numbers Service

This is very similar to how we built the strings_service so I will be giving a little bit of instructions and code, but not much of an explanation as last time

Create a new folder for /workspaces/api_dialing_grpc/numbers_service

Initialize the module

Include the module in the workspace

From the /workspaces/api_dialing_grpc command prompt

mkdir numbers_service
cd numbers_service
go mod init github.com/woodman231/api_dialing_grpc/numbers_service
cd ..
go work use ./numbers_service
Modify the code for /workspaces/api_dialing_grpc/numbers_service/go.mod with the following code:
module github.com/woodman231/api_dialing_grpc/numbers_service

go 1.18

replace github.com/woodman231/api_dialing_grpc/protos => ../protos
Create a file in the numbers_service folder called “main.go”. Give it the following code
package main

import (
    "context"
    "flag"
    "fmt"
    "log"
    "net"

    "google.golang.org/grpc"

    pb "github.com/woodman231/api_dialing_grpc/protos/numberspb"
)

var (
    port = flag.Int("port", 50052, "The server port")
)

// server is used to implement NumbersServiceServer
type server struct {
    pb.UnimplementedNumberServiceServer
}

// AddTwoNumbers implements NumbersServiceServer.AddTwoNumbers
func (s *server) AddTwoNumbers(ctx context.Context, in *pb.OperationRequest) (*pb.OperationResult, error) {
    numberOne := in.GetInputNumberOne()
    numberTwo := in.GetInputNumberTwo()

    log.Printf("Received: AddTwoNumbers Request %v, %v", numberOne, numberTwo)

    result := numberOne + numberTwo

    return &pb.OperationResult{
        OutputNumber: result,
    }, nil
}

// SubtractTwoNumbers implments NumbersServiceServer.SubtractTwoNumbers
func (s *server) SubtractTwoNumbers(ctx context.Context, in *pb.OperationRequest) (*pb.OperationResult, error) {
    numberOne := in.GetInputNumberOne()
    numberTwo := in.GetInputNumberTwo()

    log.Printf("Received: SubtractTwoNumbers Request %v, %v", numberOne, numberTwo)

    result := numberOne - numberTwo

    return &pb.OperationResult{
        OutputNumber: result,
    }, nil
}

func main() {
    flag.Parse()

    lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }

    s := gRPC.NewServer()
    pb.RegisterNumberServiceServer(s, &server{})
    log.Printf("server listening at %v", lis.Addr())
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}
Take note that this time we are using server port 50052 for this microservice. Once again we are creating a struct and adding members to it in order to satisfy the Service interface that was generated in the /protos/numberspb/numbers_service_grpc.pb.go file.

Test the Numbers Service

In a command prompt at /workspaces/api_dialing_grpc/numbers_service execute the following commands

go mod tidy
go run main.go
You will notice that this service is now running on port 50052

In your Postman and in the Workspace that you created earlier create a new API called numbers_service and import the .proto file from your computer for the numbers_service

Then create a new gRPC request

This time the url wil be gRPC://localhost:50052, and the API you will select is the numbers_service and the AddTwoNumbers method

Generate an example and Invoke it

Your Postman should look something like this.

Your terminal should look something like this:
Save this request in a new collection called numbers_service.

Create a new request for the SubtractTwoNumbers method and save that request in to the numbers_service collection as well.

When your done testing it out return to Visual Studio Code and do a CTRL+C to stop the numbers_service

Test the Strings and Numbers Service at the same time

With a command prompt open in Visual Studio Code you are able to have multiple instances. Assuming that you have one running now via CTRL+SHIFT+`, you can click on the + button to add a new instance.

You can then switch which instance you have open by clicking the appropriate tab.
In one command prompt change directories to /workspaces/api_dialing_grpc/strings_service, and then in the other change directories to /workspaces/api_dialing_grpc/numbers_service. Then in each of them execute.
go run main.go
Doing so will also make the tab names change a bit.
Return to your Postman and you should be able to make these saved requests to each of the 4 endpoints across the strings and numbers services. Feel free to hit the “Generate example message” multiple times for each end point and test at your hearts content.

Your strings_service terminal should look something like this:

Your numbers_service terminal should look something like this:
When you are done with either service do the CTRL+C to exit out of it.

Build the API / HTTP Service

Starting with a command prompt at /workspaces/api_dialing_grpc execute the following commands:

mkdir api
cd api
go mod init github.com/woodman231/api_dialing_grpc/api
cd ..
go work use ./api
You can use a different github username if you like

Your go.work file should now look like this:

go 1.18

use (
    ./api
    ./numbers_service
    ./protos
    ./strings_service
)
Next, we need to make it so that the api can properly import the protobuff files. Modify the /api/go.mod file as follows:
module github.com/woodman231/api_dialing_grpc/api

go 1.18

replace github.com/woodman231/api_dialing_grpc/protos => ../protos
To begin we will just make a request to the MakeUpperCase method of the String service as well as a Hello World End Point so that we can confirm that the service is running.

Create a file in /api called main.go. Give it the following code:

package main

import (
    "context"
    "encoding/json"
    "flag"
    "fmt"
    "log"
    "net/http"

    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"

    stringspb "github.com/woodman231/api_dialing_grpc/protos/stringspb"
)

var (
    strings_server_host = flag.String("strings_server_host", "localhost", "The host name for the strings_server service")
    strings_server_port = flag.Int("strings_server_port", 50051, "The port for the strings_server service")
)

func main() {
    flag.Parse()

    stringsServerConnectionString := fmt.Sprintf("%v:%v", *strings_server_host, *strings_server_port)

    log.Printf("stringsServerConnectionString: %v", stringsServerConnectionString)

    // Connect to the Strings Service
    stringsServiceConnection, err := gRPC.Dial(stringsServerConnectionString, gRPC.WithTransportCredentials(insecure.NewCredentials()))

    if err != nil {
        log.Fatalf("did not connect to strings gRPC server")
    }

    defer stringsServiceConnection.Close()

    stringServiceClient := stringspb.NewStringServiceClient(stringsServiceConnection)

    // Create Mux Server
    muxServer := http.NewServeMux()

    muxServer.HandleFunc("/api/hello", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello World"))
    })

    muxServer.HandleFunc("/api/StringService/MakeUpperCase", func(w http.ResponseWriter, r *http.Request) {
        // Create a variable for the String Service OperationRequest
        var stringOperationRequest stringspb.OperationRequest

        // Decode the HTTP Request Body and Store the decoded object in the stringOperationRequest variable
        err := json.NewDecoder(r.Body).Decode(&stringOperationRequest)

        // If we are not able to decode the JSON that was provided in the request body then let the person know
        if err != nil {
            http.Error(w, err.Error(), http.StatusBadRequest)
            return
        }

        // If the input_string property was not set or was empty, then let the requester know and do not make the GRPC Request
        if stringOperationRequest.InputString == "" {
            http.Error(w, "Missing input_string, or input_string is empty", http.StatusBadRequest)
            return
        }

        ctx := context.Background()

        // Call the MakeUpperCase method of the stringServiceClient over GRPC
        gRPCResponse, err := stringServiceClient.MakeUpperCase(ctx, &stringOperationRequest)

        // If we couldn't connect to the GRPC Service let the requester know
        if err != nil {
            log.Printf("Could not get response from GRPC")
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }

        // Marshal the response to JSON to reply with in the Response Body
        marshaledResponse, err := json.Marshal(gRPCResponse)

        // If we couldn't marshal the response, then let the requester know
        if err != nil {
            log.Printf("Could not marshal response from GRPC")
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }

        // Set the content-type of the response to application/json
        // Write the marshaledResponse to the HTTP Response body
        w.Header().Set("Content-Type", "application/json")
        w.Write(marshaledResponse)
    })

    fmt.Println("Listening on port 8080")

    http.ListenAndServe(":8080", muxServer)
}
Let’s go over this for a second.

In the beginning we set some parameters for the strings_server host name, and the strings_server port number. This means that we can execute this command with parameters like “–strings_server_host=localhost”

We read the flags and build a connection string to the String Service GRPC Server

We then connect to the GRPC Service and create a client from the StringsServiceConnection.

We then create a MuxServer which is the basic HTTP Handler that is built in to the GO Framework.

We add a Handler to the MuxServer for “/api/hello” as an endpoint that we can use to just be sure basic communications over HTTP work (not necessary for production)

We then add a Handler to the MuxSErver for “/api/StringService/MakeUpperCase”. This function will read the request body, and decode it in to a StringOperationsRequest struct (Remember we defined this as a Message type in the protobuff file and the protobuff compiler created a struct for us in the strings_service_pb.go file). We then validate the request body that we got, and if it is valid we send it along to the GRPC Client Method of MakeUpperCase which will return either a valid response or an error. If an error arises, then we return the error, otherwise we attempt to deserialize the response to text and write the response to the HTTP Response body after setting the Content-Type of the HTTP Response to “application/json”.

Test the API / HTTP Service

Test the API / HTTP Service

With what we have in place we should now be able to get responses at

  • /api/hello
  • /api/StringService/MakeUpperCase


These are case sensitive routes.

We will need to use the multiple terminals trick in Visual Studio code that we discussed earlier. Be sure to run “go run main.go” both in “/workspaces/api_dialing_grpc/api” and “/workspaces/api_dialing_grpc/string_service”.

Using Postman create a new HTTP Request to localhost:8080/api/hello using the GET method

If all is well, then you should get something like this:

Save this request to a new collection called api.

Your Postman workspace should look something like this now:

Create a new HTTP Request in your Postman to localhost:8080/api/StringService/MakeUpperCase using the POST Method

Go to the body tab and select the raw radio button. Use the following body:

{
    "input_string": "hElLo WoRlD"
}
Click on Send and you should get a response from the GRPC Service.

Your Postman should look something like this:

Your API terminal should look something like this:
And your strings_service terminal should look something like this:
You can test out various invalid JSONs in your request body and the like and various input_strings that you like to see the results.

Do the CTRL+C in each terminal when you are done testing.

Refactor the API / HTTP Service

Of course, we might be tempted to just make additional MuxServer HandleFunc’s for each Method in our service, but we do want our code to be as DRY (Don’t Repeat Yourself) as possible.

Where we run in to a little bit of a problem is that when we use the GRPC Client it does expect the input to be of a specific type when using any of the methods. Currently the arguments are the same for both of the String service methods and both of the Number service methods. What is the same though is that we are getting an HTTP Request in, calling the appropriate GRPC Client method and then writing the GRPC response to the HTTP Response body.

Furthermore, when the protobuff compiler created the files for us they created a special ServiceDesciption object. So we can use the information in this object for both services to build our routes.

The method that I came up with was to create an APIService struct that had 3 properties. One of the properties is for the StringServiceClient. Another property is for the NumberServiceCLient. The last property is for the GO / MUX Http Server.

While creating those services I assigned them a value to a new instance of this struct. I created a RegisterRoutes method for that struct so that it could take advantage of the three properties mentioned before. I also created a registerRoutesForService method which takes in a GRPC Service Description object. It takes the service name and methods and registers patterns for each method name. The handleRequest method takes basically the same code path as the single request route we discussed earlier; however, where things differ to actually call the method, they are wrapped in switch statements for the service being called and the method being called.

Modify the /api/main.go file as follows:

package main

import (
    "context"
    "encoding/json"
    "flag"
    "fmt"
    "log"
    "net/http"
    "strings"

    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"

    numberspb "github.com/woodman231/api_dialing_grpc/protos/numberspb"
    stringspb "github.com/woodman231/api_dialing_grpc/protos/stringspb"
)

var (
    strings_server_host = flag.String("strings_server_host", "localhost", "The host name for the strings_server service")
    strings_server_port = flag.Int("strings_server_port", 50051, "The port for the strings_server service")
    numbers_server_host = flag.String("numbers_server_host", "localhost", "The host name for the numbers_server service")
    numbers_server_port = flag.Int("numbers_server_port", 50052, "The port for the numbers_server port")
)

type APIService struct {
    StringsServiceClient stringspb.StringServiceClient
    NumbersServiceClient numberspb.NumberServiceClient
    MuxServer            *http.ServeMux
}

func main() {
    flag.Parse()

    var apiService APIService

    stringsServerConnectionString := fmt.Sprintf("%v:%v", *strings_server_host, *strings_server_port)
    numbersServerConnectionString := fmt.Sprintf("%v:%v", *numbers_server_host, *numbers_server_port)

    log.Printf("stringsServerConnectionString: %v", stringsServerConnectionString)
    log.Printf("numbersServerConnectionString: %v", numbersServerConnectionString)

    // Connect to the Strings Service
    stringsServiceConnection, err := gRPC.Dial(stringsServerConnectionString, gRPC.WithTransportCredentials(insecure.NewCredentials()))

    if err != nil {
        log.Fatalf("did not connect to strings gRPC server")
    }

    defer stringsServiceConnection.Close()

    stringServiceClient := stringspb.NewStringServiceClient(stringsServiceConnection)
    apiService.StringsServiceClient = stringServiceClient

    // Connect to the Numbers Service
    numbersServiceConnection, err := gRPC.Dial(numbersServerConnectionString, gRPC.WithTransportCredentials(insecure.NewCredentials()))

    if err != nil {
        log.Fatalf("did not connect to numbers gRPC server")
    }

    defer numbersServiceConnection.Close()

    numbersServiceClient := numberspb.NewNumberServiceClient(numbersServiceConnection)
    apiService.NumbersServiceClient = numbersServiceClient

    // Create Mux Server
    muxServer := http.NewServeMux()
    apiService.MuxServer = muxServer

    apiService.RegisterRoutes()

    fmt.Println("Listening on port 8080")

    http.ListenAndServe(":8080", muxServer)
}

func (a *APIService) RegisterRoutes() {
    // Register a hello world route to be sure that the service is running
    a.MuxServer.HandleFunc("/api/hello", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello World"))
    })

    // Register routes for each service
    a.registerRoutesForService(stringspb.StringService_ServiceDesc)
    a.registerRoutesForService(numberspb.NumberService_ServiceDesc)
}

func (a *APIService) registerRoutesForService(serviceDesc gRPC.ServiceDesc) {
    // Shorten the service name
    shortServiceName := getShortServiceName(serviceDesc.ServiceName)

    // For each method in the service description create a route for the API
    for i := range serviceDesc.Methods {
        methodName := serviceDesc.Methods[i].MethodName

        pattern := fmt.Sprintf("/api/%v/%v", shortServiceName, methodName)

        fmt.Printf("Registering: %v.%v @ %v\n", shortServiceName, methodName, pattern)

        a.MuxServer.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
            a.handleRequest(shortServiceName, methodName, w, r)
        })
    }
}

func getShortServiceName(currentServiceName string) string {
    words := strings.Split(currentServiceName, ".")
    wordCount := len(words)

    if wordCount > 0 {
        return words[wordCount-1]
    }

    return words[0]
}

func (a *APIService) handleRequest(serviceName string, methodName string, w http.ResponseWriter, r *http.Request) {
    log.Printf("Received request for %v", r.URL.String())

    // Set Up Some Variables to Hold our gRPCResponse, and possible gRPCError
    var gRPCResponse interface{}
    var gRPCErr error

    switch serviceName {
    case "StringService":
        var stringOperationRequest stringspb.OperationRequest

        // Convert the HTTP Request Body to a StringService OperationRequest
        decodeErr := json.NewDecoder(r.Body).Decode(&stringOperationRequest)

        // Validate the request
        if decodeErr != nil {
            http.Error(w, decodeErr.Error(), http.StatusBadRequest)
            return
        }

        if stringOperationRequest.InputString == "" {
            http.Error(w, "Missing input_string, or input_string is empty", http.StatusBadRequest)
            return
        }

        // Use the decoded request body with the appropriate method that was passed in
        switch methodName {
        case "MakeUpperCase":
            gRPCResponse, gRPCErr = a.StringsServiceClient.MakeUpperCase(context.Background(), &stringOperationRequest)
        case "MakeLowerCase":
            gRPCResponse, gRPCErr = a.StringsServiceClient.MakeLowerCase(context.Background(), &stringOperationRequest)
        default:
            http.Error(w, "Not Implemented", http.StatusInternalServerError)
            return
        }
    case "NumberService":
        var numberOperationRequest numberspb.OperationRequest

        // Convert the HTTP Request Body to a NumbersSErvice OperationRequest
        decodeErr := json.NewDecoder(r.Body).Decode(&numberOperationRequest)

        // Validate the request
        if decodeErr != nil {
            http.Error(w, decodeErr.Error(), http.StatusBadRequest)
            return
        }

        if numberOperationRequest.InputNumberOne == 0 && numberOperationRequest.InputNumberTwo == 0 {
            http.Error(w, "Either input_number_one or input_number_two are missing or they are both 0", http.StatusBadRequest)
            return
        }

        // Use the decoded request body with the appropriate method that was passed in
        switch methodName {
        case "AddTwoNumbers":
            gRPCResponse, gRPCErr = a.NumbersServiceClient.AddTwoNumbers(context.Background(), &numberOperationRequest)
        case "SubtractTwoNumbers":
            gRPCResponse, gRPCErr = a.NumbersServiceClient.SubtractTwoNumbers(context.Background(), &numberOperationRequest)
        default:
            http.Error(w, "Not Implemented", http.StatusInternalServerError)
            return
        }
    }

    // Write the error from the GRPC Client if applicable
    if gRPCErr != nil {
        log.Print("Could not get response from GRPC")
        http.Error(w, gRPCErr.Error(), http.StatusInternalServerError)
        return
    }

    // Write the response from the GRPC Client if applicable
    marshaledResponse, marshalErr := json.Marshal(gRPCResponse)

    if marshalErr != nil {
        log.Printf("Could not marshal response from GRPC")
        http.Error(w, marshalErr.Error(), http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    w.Write(marshaledResponse)
}

Test the API / HTTP, String, and Number Services at the same time

Use the multiple terminal trick in Visual Studio code that we talked about earlier to have 3 terminals open. Execute “go run main.go” inside of each service (/workspaces/api_dialing_grpc/api, /workspaces/api_dialing_grpc/strings_service, /workspaces/api_dialing_grpc/numbers_service).

When they are all running use Postman to create a new HTTP Request to localhost:8080/api/StringService/MakeLowerCase, it will be a POST again, and use the same body that you used from the MakeUpperCase method.

Your Postman request should look like this:

Your API terminal should look like this:
Your strings_service terminal should look like this:
In Postman create another HTTP Request to localhost:8080/api/NumberService/AddTwoNumbers. It will be a post request and you should use a body similar to this one. Feel free to use any other numbers that you like. Save it to the api collection.
{
    "input_number_one": 8080,
    "input_number_two": 5050
}

In Postman create another HTTP Request to localhost:8080/api/NumbersService/SubtractTwoNumbers. Give it the same body as you gave the AddTwoNumbers method. Feel free to use different numbers.

Your collection in Postman should look something like this

When you are done testing do a CTRL+C in each of the terminals to stop the running services.

Conclusion

Thus far we have built our individual services and a proxy to accept HTTP requests and translate those to gRPC requests. Each service is running as it’s own microservice on our development environment host. Next up we will set up these microservices to run in their own Docker Container.

Show me the code: https://github.com/woodman231/api_dialing_grpc

Part 1: Setting Up The Development Container And Building The Services
Part 2: Running Each Service In Its Own Docker Container — Link
Part 3: Debug Each Service While It Is Running In Its Own Docker Container — Link

About Intertech

Intertech is a Software Development Consulting Firm that provides single and multiple turnkey software development teams, available on your schedule and configured to achieve success as defined by your requirements independently or in co-development with your team. Intertech teams combine proven full-stack, DevOps, Agile-experienced lead consultants with Delivery Management, User Experience, Software Development, and QA experts in Business Process Automation (BPA), Microservices, Client- and Server-Side Web Frameworks of multiple technologies, Custom Portal and Dashboard development, Cloud Integration and Migration (Azure and AWS), and so much more. Each Intertech employee leads with the soft skills necessary to explain complex concepts to stakeholders and team members alike and makes your business more efficient, your data more valuable, and your team better. In addition, Intertech is a trusted partner of more than 4000 satisfied customers and has a 99.70% “would recommend” rating.