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

May 6, 2025 - 16:06
 0
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:

  1. Strong Typing: Contract-first approach with Protocol Buffers
  2. Bi-directional Streaming: Supports streaming in both directions
  3. Language Agnostic: Works with multiple programming languages
  4. High Performance: Uses HTTP/2 for efficient communication
  5. 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

  1. Use TLS in Production: Always secure your gRPC connections with TLS certificates in production environments

  2. Health Checking: Implement health checking to allow clients to verify if the server is operational:

   service Health {
     rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
   }
  1. Error Handling: Use gRPC status codes consistently to communicate errors

  2. Logging and Monitoring: Implement middleware for logging and monitoring gRPC calls

  3. Versioning: Include versioning in your package names or service definitions

  4. Backwards Compatibility: Make changes to your proto files in a backward-compatible way

  5. Contract Testing: Create automated tests to verify service contracts

  6. Timeout Handling: Set appropriate timeouts for all gRPC calls

Advanced gRPC Features

As your microservices grow, consider exploring these advanced gRPC features:

  1. Streaming: Implement server streaming, client streaming, or bidirectional streaming for efficiency

  2. Load Balancing: Configure proper load balancing for gRPC services

  3. Connection Pooling: Reuse gRPC connections instead of creating new ones for each request

  4. Interceptors: Use interceptors (middleware) for cross-cutting concerns like authentication and logging

  5. 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