Migrating REST APIs to gRPC Using Protocol Buffers in Golang
Introduction
Migrating from REST to gRPC can offer several benefits, including improved performance, smaller payload sizes, and support for streaming data. In this blog, we'll walk through the steps to migrate a simple REST API to gRPC using Protocol Buffers in Golang. We'll cover the key differences between REST and gRPC, and demonstrate how to implement and test the new gRPC service.
Setting Up Your Environment
Ensure you have Golang and Protocol Buffers installed. Refer to the installation steps in previous blogs if needed. We'll use a simple user management service as an example.
Defining the REST API
Let's start by defining a simple REST API for user management. Create a new directory for the project:
bash
mkdir rest-to-grpc-example
cd rest-to-grpc-example
Create a file named main.go
for the REST API:
go
package main
import (
"encoding/json"
"log"
"net/http"
"strconv"
"github.com/gorilla/mux"
)
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email"`
}
var users = make(map[string]User)
func CreateUser(w http.ResponseWriter, r *http.Request) {
var user User
_ = json.NewDecoder(r.Body).Decode(&user)
user.ID = "user_" + strconv.Itoa(len(users)+1)
users[user.ID] = user
json.NewEncoder(w).Encode(user)
}
func GetUser(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
user, exists := users[params["id"]]
if !exists {
http.Error(w, "User not found", http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(user)
}
func main() {
router := mux.NewRouter()
router.HandleFunc("/users", CreateUser).Methods("POST")
router.HandleFunc("/users/{id}", GetUser).Methods("GET")
log.Fatal(http.ListenAndServe(":8000", router))
}
Run the REST API server:
bash
go run main.go
You can test the REST API using curl
:
bash
# Create a new user
curl -X POST -H "Content-Type: application/json" -d '{"name": "Alice", "age": 30, "email": "alice@example.com"}' http://localhost:8000/users
# Get the user
curl http://localhost:8000/users/user_1
Defining the gRPC Service
Next, we'll define a gRPC service for the same functionality. Create a file named user.proto
:
proto
syntax = "proto3"; package user; service UserService { rpc CreateUser (CreateUserRequest) returns (CreateUserResponse); rpc GetUser (GetUserRequest) returns (GetUserResponse); } message CreateUserRequest { string name = 1; int32 age = 2; string email = 3; } message CreateUserResponse { string id = 1; } message GetUserRequest { string id = 1; } message GetUserResponse { string id = 1; string name = 2; int32 age = 3; string email = 4; }
Generate the Go code from the .proto
file:
bash
protoc --go_out=. --go-grpc_out=. user.proto
This will generate user.pb.go
and user_grpc.pb.go
.
Implementing the gRPC Server
Create a file named server.go
for the gRPC server:
go
package main
import (
"context"
"log"
"net"
"strconv"
"google.golang.org/grpc"
pb "path/to/your/generated/proto/files"
)
type server struct {
pb.UnimplementedUserServiceServer
users map[string]*pb.GetUserResponse
}
func newServer() *server {
return &server{users: make(map[string]*pb.GetUserResponse)}
}
func (s *server) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) {
id := "user_" + strconv.Itoa(len(s.users)+1)
user := &pb.GetUserResponse{
Id: id,
Name: req.GetName(),
Age: req.GetAge(),
Email: req.GetEmail(),
}
s.users[id] = user
return &pb.CreateUserResponse{Id: id}, nil
}
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) {
user, exists := s.users[req.GetId()]
if !exists {
return nil, fmt.Errorf("user not found")
}
return user, nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterUserServiceServer(s, newServer())
log.Printf("server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
Run the gRPC server:
bash
go run server.go
Implementing the gRPC Client
Create a file named client.go
for the gRPC client:
go
package main
import (
"context"
"log"
"time"
"google.golang.org/grpc"
pb "path/to/your/generated/proto/files"
)
func main() {
conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewUserServiceClient(conn)
// Create a new user
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
createUserResponse, err := c.CreateUser(ctx, &pb.CreateUserRequest{Name: "Bob", Age: 25, Email: "bob@example.com"})
if err != nil {
log.Fatalf("could not create user: %v", err)
}
log.Printf("Created user with ID: %s", createUserResponse.GetId())
// Get the user
getUserResponse, err := c.GetUser(ctx, &pb.GetUserRequest{Id: createUserResponse.GetId()})
if err != nil {
log.Fatalf("could not get user: %v", err)
}
log.Printf("User details: %+v", getUserResponse)
}
Run the gRPC client:
bash
go run client.go
Migrating REST API to gRPC
To migrate the REST API to gRPC, we'll integrate the gRPC server with a REST gateway. This allows existing REST clients to continue using the API while transitioning to gRPC.
First, install the grpc-gateway
package:
bash
go get -u github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway go get -u github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2
Generate the gateway code from the .proto
file:
bash
protoc --grpc-gateway_out=. --grpc-gateway_opt=logtostderr=true --grpc-gateway_opt=paths=source_relative user.proto
This will generate user.pb.gw.go
.
Implementing the gRPC Gateway
Create a file named gateway.go
:
go
package main
import (
"context"
"log"
"net/http"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"google.golang.org/grpc"
"path/to/your/generated/proto/files"
)
func run() error {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
mux := runtime.NewServeMux()
opts := []grpc.DialOption{grpc.WithInsecure()}
err := pb.RegisterUserServiceHandlerFromEndpoint(ctx, mux, "localhost:50051", opts)
if err != nil {
return err
}
return http.ListenAndServe(":8080", mux)
}
func main() {
if err := run(); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
Run the gateway server:
bash
go run gateway.go
You can now access the gRPC service via REST endpoints:
bash
Comments
Post a Comment