Building Microservices with gRPC: A Practical Guide
In today's cloud-native world, microservices architecture has become a standard approach for building scalable, maintainable, and resilient applications. While REST APIs have traditionally been the go-to choice for communication between microservices, gRPC offers compelling advantages that make it an excellent alternative. This blog post will walk you through setting up a practical microservice architecture using gRPC, with a Go service as the backend and an Express.js service as the frontend. We'll cover everything from defining protocol buffers to containerizing the services with Docker. What is gRPC? gRPC (gRPC Remote Procedure Calls) is a high-performance, open-source framework developed by Google. It allows services to communicate efficiently using HTTP/2 as the transport protocol and Protocol Buffers (protobuf) for serializing structured data. Key advantages of gRPC include: Strong Typing: Contract-first approach with Protocol Buffers Bi-directional Streaming: Supports streaming in both directions Language Agnostic: Works with multiple programming languages High Performance: Uses HTTP/2 for efficient communication Code Generation: Automatically generates client and server code Project Structure Our demo project has the following structure: /grpc-demo ├── docker-compose.yml ├── express-service/ # Node.js frontend service │ ├── Dockerfile │ ├── package.json │ └── src/ │ ├── app.js │ ├── controllers/ │ ├── generated/ # Generated gRPC code │ └── routes/ ├── go-service/ # Go backend service │ ├── Dockerfile │ ├── go.mod │ ├── go.sum │ ├── main.go │ ├── api/ │ ├── grpcgoexpress/ # Generated gRPC code │ └── models/ └── proto/ # Protocol Buffer definitions └── service.proto Step 1: Define Your Protocol Buffers The first step in building a gRPC-based microservice is defining your service contract using Protocol Buffers. Create a .proto file: syntax = "proto3"; package grpcgoexpress; option go_package = "github.com/go-service/grpcgoexpress;grpcgoexpress"; // The greeting service definition. service GreetingService { // Sends a greeting rpc GetData (RequestMessage) returns (ResponseMessage); } // The request message containing the user's query. message RequestMessage { string query = 1; } // The response message containing the data. message ResponseMessage { string data = 1; } This simple definition outlines a service with one method (GetData) that accepts a RequestMessage and returns a ResponseMessage. Step 2: Generate Code from Protocol Buffers For Go Service To generate Go code from your .proto file, install the required tools: go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28 go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2 Then generate the code: protoc --go_out=. --go-grpc_out=. proto/service.proto This will create two files in your specified package directory (grpcgoexpress/): service.pb.go: Contains message type definitions service_grpc.pb.go: Contains the service interface and client implementation For Node.js Service For Node.js, install the required packages: npm install --save @grpc/grpc-js @grpc/proto-loader Then generate the JavaScript code: npx grpc_tools_node_protoc \ --js_out=import_style=commonjs,binary:./express-service/src/generated \ --grpc_out=grpc_js:./express-service/src/generated \ --proto_path=./proto \ ./proto/service.proto This will generate: service_pb.js: Message type definitions service_grpc_pb.js: Service client and server interfaces Step 3: Implement the gRPC Server in Go Now, implement the gRPC server in Go: package main import ( "context" "log" "net" pb "github.com/go-service/grpcgoexpress" "google.golang.org/grpc" ) type server struct { pb.UnimplementedGreetingServiceServer } func (s *server) GetData(ctx context.Context, req *pb.RequestMessage) (*pb.ResponseMessage, error) { log.Printf("Received: %s", req.Query) return &pb.ResponseMessage{Data: "Hello from Go server!"}, nil } func main() { lis, err := net.Listen("tcp", ":50051") if err != nil { log.Fatalf("failed to listen: %v", err) } s := grpc.NewServer() pb.RegisterGreetingServiceServer(s, &server{}) log.Println("Server is running on port :50051") if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } } Step 4: Create a gRPC Client in Node.js/Express Implement a client in your Express.js application: const grpc = require('@grpc/grpc-js'); const express = require('express'); // Load the pre-generated gRPC code const protoServices = require('./generated/service_grpc_pb'); const protoMessages = require('./generated/service_pb'); // Use environment variable for gRPC server address or fallback to local

In today's cloud-native world, microservices architecture has become a standard approach for building scalable, maintainable, and resilient applications. While REST APIs have traditionally been the go-to choice for communication between microservices, gRPC offers compelling advantages that make it an excellent alternative.
This blog post will walk you through setting up a practical microservice architecture using gRPC, with a Go service as the backend and an Express.js service as the frontend. We'll cover everything from defining protocol buffers to containerizing the services with Docker.
What is gRPC?
gRPC (gRPC Remote Procedure Calls) is a high-performance, open-source framework developed by Google. It allows services to communicate efficiently using HTTP/2 as the transport protocol and Protocol Buffers (protobuf) for serializing structured data.
Key advantages of gRPC include:
- Strong Typing: Contract-first approach with Protocol Buffers
- Bi-directional Streaming: Supports streaming in both directions
- Language Agnostic: Works with multiple programming languages
- High Performance: Uses HTTP/2 for efficient communication
- Code Generation: Automatically generates client and server code
Project Structure
Our demo project has the following structure:
/grpc-demo
├── docker-compose.yml
├── express-service/ # Node.js frontend service
│ ├── Dockerfile
│ ├── package.json
│ └── src/
│ ├── app.js
│ ├── controllers/
│ ├── generated/ # Generated gRPC code
│ └── routes/
├── go-service/ # Go backend service
│ ├── Dockerfile
│ ├── go.mod
│ ├── go.sum
│ ├── main.go
│ ├── api/
│ ├── grpcgoexpress/ # Generated gRPC code
│ └── models/
└── proto/ # Protocol Buffer definitions
└── service.proto
Step 1: Define Your Protocol Buffers
The first step in building a gRPC-based microservice is defining your service contract using Protocol Buffers. Create a .proto
file:
syntax = "proto3";
package grpcgoexpress;
option go_package = "github.com/go-service/grpcgoexpress;grpcgoexpress";
// The greeting service definition.
service GreetingService {
// Sends a greeting
rpc GetData (RequestMessage) returns (ResponseMessage);
}
// The request message containing the user's query.
message RequestMessage {
string query = 1;
}
// The response message containing the data.
message ResponseMessage {
string data = 1;
}
This simple definition outlines a service with one method (GetData
) that accepts a RequestMessage
and returns a ResponseMessage
.
Step 2: Generate Code from Protocol Buffers
For Go Service
To generate Go code from your .proto
file, install the required tools:
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
Then generate the code:
protoc --go_out=. --go-grpc_out=. proto/service.proto
This will create two files in your specified package directory (grpcgoexpress/
):
-
service.pb.go
: Contains message type definitions -
service_grpc.pb.go
: Contains the service interface and client implementation
For Node.js Service
For Node.js, install the required packages:
npm install --save @grpc/grpc-js @grpc/proto-loader
Then generate the JavaScript code:
npx grpc_tools_node_protoc \
--js_out=import_style=commonjs,binary:./express-service/src/generated \
--grpc_out=grpc_js:./express-service/src/generated \
--proto_path=./proto \
./proto/service.proto
This will generate:
-
service_pb.js
: Message type definitions -
service_grpc_pb.js
: Service client and server interfaces
Step 3: Implement the gRPC Server in Go
Now, implement the gRPC server in Go:
package main
import (
"context"
"log"
"net"
pb "github.com/go-service/grpcgoexpress"
"google.golang.org/grpc"
)
type server struct {
pb.UnimplementedGreetingServiceServer
}
func (s *server) GetData(ctx context.Context, req *pb.RequestMessage) (*pb.ResponseMessage, error) {
log.Printf("Received: %s", req.Query)
return &pb.ResponseMessage{Data: "Hello from Go server!"}, nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterGreetingServiceServer(s, &server{})
log.Println("Server is running on port :50051")
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
Step 4: Create a gRPC Client in Node.js/Express
Implement a client in your Express.js application:
const grpc = require('@grpc/grpc-js');
const express = require('express');
// Load the pre-generated gRPC code
const protoServices = require('./generated/service_grpc_pb');
const protoMessages = require('./generated/service_pb');
// Use environment variable for gRPC server address or fallback to localhost
const grpcServerAddress = process.env.GRPC_SERVER || 'localhost:50051';
console.log(`Connecting to gRPC server at: ${grpcServerAddress}`);
// Create a gRPC client
const client = new protoServices.GreetingServiceClient(
grpcServerAddress,
grpc.credentials.createInsecure()
);
// Create an Express application
const app = express();
app.get('/data', (req, res) => {
const query = req.query.query || 'default query';
// Create a request message using the generated code
const request = new protoMessages.RequestMessage();
request.setQuery(query);
client.getData(request, (err, response) => {
if (err) {
console.error('Error:', err);
return res.status(500).send(err);
}
res.send({
data: response.getData()
});
});
});
app.listen(3000, () => {
console.log('Express server running on http://localhost:3000');
});
Step 5: Containerize Your Services with Docker
Go Service Dockerfile
FROM golang:1.20-alpine AS builder
WORKDIR /app
COPY . .
RUN go mod download
RUN go build -o /app/server .
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/server /app/server
EXPOSE 50051
CMD ["/app/server"]
Express.js Service Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "src/app.js"]
Step 6: Orchestrate with Docker Compose
Create a docker-compose.yml
file to manage both services:
version: '3.8'
services:
go-service:
build:
context: ./go-service
dockerfile: Dockerfile
ports:
- "50051:50051"
networks:
- app-network
restart: unless-stopped
healthcheck:
test: ["CMD", "nc", "-z", "localhost", "50051"]
interval: 10s
timeout: 5s
retries: 5
express-service:
build:
context: ./express-service
dockerfile: Dockerfile
ports:
- "3000:3000"
networks:
- app-network
depends_on:
- go-service
restart: unless-stopped
environment:
- GRPC_SERVER=go-service:50051
networks:
app-network:
driver: bridge
Running the Microservices
Start the services with:
docker-compose up -d
This will build and start both services. The Express.js service will wait for the Go service to be ready before starting, thanks to the depends_on
configuration.
Testing the Setup
Once your services are running, you can test the Express.js endpoint:
curl "http://localhost:3000/data?query=hello"
The response should look like:
{
"data": "Hello from Go server!"
}
Best Practices for gRPC in Microservices
Use TLS in Production: Always secure your gRPC connections with TLS certificates in production environments
Health Checking: Implement health checking to allow clients to verify if the server is operational:
service Health {
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
}
Error Handling: Use gRPC status codes consistently to communicate errors
Logging and Monitoring: Implement middleware for logging and monitoring gRPC calls
Versioning: Include versioning in your package names or service definitions
Backwards Compatibility: Make changes to your proto files in a backward-compatible way
Contract Testing: Create automated tests to verify service contracts
Timeout Handling: Set appropriate timeouts for all gRPC calls
Advanced gRPC Features
As your microservices grow, consider exploring these advanced gRPC features:
Streaming: Implement server streaming, client streaming, or bidirectional streaming for efficiency
Load Balancing: Configure proper load balancing for gRPC services
Connection Pooling: Reuse gRPC connections instead of creating new ones for each request
Interceptors: Use interceptors (middleware) for cross-cutting concerns like authentication and logging
Reflection: Enable server reflection to help with debugging and testing
Conclusion
gRPC offers a powerful way to connect microservices with strongly typed contracts, efficient communication, and excellent language interoperability. By following this guide, you've set up a basic microservice architecture with a Go backend and Node.js frontend communicating via gRPC.
This setup is just the beginning - as your application grows, gRPC provides the scalability and performance needed for demanding microservice ecosystems.
The complete code for this tutorial is available in the project repository, where you can explore the implementation in more detail.
Happy coding