In the first part I was talking about building simplest gRPC API. But the real world is complex and real APIs rarely look like a calculator. Today we will be looking at layered application which consists of number of parts or layers. Our task is to expose data from database to the world through gRPC API. This time we will be writing Order API.
Let’s go!
Application layers
Our application has several layers such as:
Transport: gRPC
Transport layer is a mechanism which passes data between data source (business logic) and client. It sends what it was told to send and it knows how to prepare data for transfer, i.e. encode/decode. We use gRPC, which uses HTTP/2 as transport.
This layer consists of two parts:
- auto-generated
*.pb.go
file. - manually written gRPC server implementation.
Business logic: service
Business logic does some meaningful work, it takes data somewhere, very often from database and processes it somehow. It can call other external (micro)services for any purposes. It can cache data and so on.
This part of code doesn’t know anything about how this processed data will be transferred to the client, i.e. it’s transport-independent and it exposes an interface which will be used by transport layer.
Database: Repo
Repo is one more part of our application which works directly with database and any other data store. It also exposes an interface which can be used by Business Logic.
All this layers all together look like this:
Connect business logic and transport
In the previous article, part called gRPC server implementation shows how easily server can be implemented, but that part didn’t include any interactions with service layer. So, let’s fix it!
Our app will have next file structure:
% tree
.
├── client
│ └── main.go
├── pb
│ ├── order.pb.go
│ └── order.proto
├── server
│ ├── grpc_server.go
│ └── main.go
└── service
└── service.go
First of all we define API interface with Protobuf.
Protobuf service: order.proto
syntax = "proto3";
option go_package = "pb";
service OrderService {
rpc Get(GetRequest) returns (GetResponse);
rpc Add(AddRequest) returns (AddResponse);
}
message Item {
int64 id = 1;
string name = 2;
int32 qty = 3;
}
message Order {
int64 id = 1;
string number = 2;
string status = 3;
int64 created_at = 4;
repeated Item items = 5;
}
message GetRequest {
int64 id = 1;
}
message GetResponse {
Order order = 1;
}
message AddRequest {
message Body {
Order order = 1;
}
Body body = 1;
}
message AddResponse {
int64 id = 1;
string status = 2;
}
This API has two methods:
Get
which returns an Order by ID.Add
which adds an Order.
Each of these methods accepts request object, message with <method>Request
name and returns response, message with <method>Response
. Later we’ll implement
gRPC server and it will be a place where we connect our transport level and
business logic, but first of all we have to define business logic itself.
For simplicity we will not use repo, but we assume it used inside service.
Business logic: service.go
package service
import (
"context"
"errors"
"strconv"
)
type (
OrderItem struct {
ID int
Name string
Qty int
State string
}
Order struct {
ID int
Num string
Status string
Items []OrderItem
Comment string
}
// OrderSvc is an interface which represents business logic.
OrderSvc interface {
Get(context.Context, int) (*Order, error)
Add(context.Context, *Order) (int, error)
}
)
type osvc struct{}
func New() OrderSvc {
return &osvc{}
}
// Get returns an Order by ID with OrderItems.
//
// Actually this function have to call repo, but for simplicity it
// returns faked data.
func (s *osvc) Get(ctx context.Context, id int) (*Order, error) {
switch id {
case 1, 2, 3:
items := []OrderItem{}
for i := 0; i < id; i++ {
items = append(items, OrderItem{
ID: (i + 1) * 10,
Name: "order_item_" + strconv.Itoa(i),
Qty: (i + 1) * 2,
})
}
return &Order{
ID: id,
Num: "order_" + strconv.Itoa(id),
Status: "created",
Items: items,
Comment: "none",
}, nil
}
return nil, errors.New("order not found")
}
// Add does something to store order and returns faked added ID.
func (s *osvc) Add(ctx context.Context, o *Order) (int, error) {
return 5, nil
}
Business logic exposes an interface OrderSvc
which is different from
pb.OrderServiceServer
it even has similar methods.
gRPC server implementation: grpc_server.go
// server implements gRPC Server OrderServiceServer interface from
// pb/order.pb.go.
type server struct {
osvc service.OrderSvc
}
func (s *server) Get(ctx context.Context, req *pb.GetRequest) (*pb.GetResponse, error) {
log.Printf("GET request: %#v\n", req)
o, err := s.osvc.Get(ctx, int(req.Id)) // call business logic
if err != nil {
return nil, err
}
items := []*pb.Item{}
// transform OrderItem into []*pb.Items field by field.
for _, i := range o.Items {
items = append(items, &pb.Item{
Id: int64(i.ID),
Name: i.Name,
Qty: int32(i.Qty),
})
}
// transform Order into *pb.Order field by field because
// service.Order and pb.Order are two different structure types.
order := &pb.Order{
Id: int64(o.ID),
Number: o.Num,
Status: o.Status,
CreatedAt: time.Now().Unix(),
Items: items,
}
return &pb.GetResponse{
Order: order,
}, nil
}
func (s *server) Add(ctx context.Context, req *pb.AddRequest) (*pb.AddResponse, error) {
log.Printf("ADD request: %#v\n", req)
o := req.Body.Order
// Here we have to done opposite work, convert pb.Order into service.Order.
id, err := s.osvc.Add(ctx, &service.Order{
ID: int(o.Id), // as well as cast particular fields.
Num: o.Number,
Status: o.Status,
Items: nil, // skip it
Comment: "new order",
})
if err != nil {
return nil, err
}
return &pb.AddResponse{
Id: int64(id),
Status: "added",
}, nil
}
It’s time to build and run our server and client. First we start server:
% cd server
% go build && ./server
at this time it has nothing to say.
Then we’ll build and start client:
% cd client
% go buils && ./client
after we run these commands client will print next info:
% ./client
order:
ID: 2, Number: order_2, Status: created
item: ID: 10, Name: order_item_0, Qty: 2
item: ID: 20, Name: order_item_1, Qty: 4
new order ID: 5, Status: added
while server, in its turn, prints which request and responses it have got:
2019/12/15 13:02:35 GET request: &pb.GetRequest{Id:2, XXX_NoUnkeyedLiteral:struct {}{}, XXX_unrecognized:[]uint8(nil), XXX_sizecache:0}
2019/12/15 13:02:35 ADD request: &pb.AddRequest{Body:(*pb.AddRequest_Body)(0xc000174630), XXX_NoUnkeyedLiteral:struct {}{}, XXX_unrecognized:[]uint8(nil), XXX_sizecache:0}
Conclusion
gRPC API is pretty simple and straightforward to implement, but at the same time it could be difficult to link transport and business logic, because of this it is necessary to transform all structures between service and transport layers.