Tutorial: REST API com Go e gRPC
Introdução
Nesse tutorial, vou mostrar como criar uma API simples usando a linguagem Go e gRPC. Após criar a API gRPC mostrarei como podemos transformá-la em uma API REST.
Você pode encontrar o código deste tutorial aqui.
O que é gRPC?
gRPC é um framework de alta performance para chamadas de procedimento remoto (RPC em inglês). Na prática, gRPC é um framework que lida apenas com a parte de comunicação em rede, então ele é bem leve e não opina em como você deve estruturar o seu projeto, ou qual banco de dados ou ORM você deve usar.
Por que usar gRPC?
O gRPC possui algumas vantagens em relação ao REST tradicional:
-
Possui um "contrato" de API definido em um arquivo próprio ".proto";
-
Utiliza protobufs por padrão para codificar as mensagens, enquanto REST geralmente usa JSON. Isso faz com que as mensagens tenham um tamanho menor e consequentemente melhora a performance de rede;
-
Possui um número menor de StatusCodes, o que facilita entender os erros retornados pela API;
-
O framework (pelo menos em Go) é fácil de utilizar.
Mas nem tudo são flores... Alguns pontos negativos são:
-
O setup inicial do gRPC é um pouco mais complicado do que de uma API REST;
-
As ferramentas para testes de APIs gRPC, como Postman, ainda não são muito maduras;
-
O gRPC ainda não é tão difundido como o REST, o que pode dificultar o uso da sua API por outros usuários.
Pré requisitos
Esse tutorial assume que você tenha alguma familiaridade com a linguagem Go e que possui os seguintes programas instalados:
-
git;
-
make;
-
curl;
-
go v1.18 ou superior.
Para seguir o tutorial, precisaremos instalar alguns programas, certifique-se de
que você possui o diretório onde o Go instala os programas no seu $PATH
, caso
não tenha adicione o seguinte ao seu ~/.bashrc
ou ~/.zshrc
:
~/.bashrc:
export PATH=$HOME/go/bin:$PATH
E execute source ~/.bashrc
.
Instale o Buf, que é um gerenciador de pacotes proto:
go install github.com/bufbuild/buf/cmd/buf@latest
Instale os plugins protoc que serão utilizados para gerar código gRPC a partir do arquivo proto:
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
Instale o grpcurl, que é equivalente ao curl para gRPC:
go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
Estruturando o projeto
Primeiro vamos criar um diretório para o nosso projeto:
mkdir tutorial-grpc
cd tutorial-grpc
git init
Agora vamos criar um Go module (Go Modules é o sistema de gerenciamento de dependências da linguagem, equivalente ao npm, só que em Go):
go mod init github.com/<SEU USUÁRIO>/tutorialgrpc
Uma vez criado o nosso módulo, vamos definir a estrutura do nosso projeto:
mkdir -p cmd/api cmd/gateway server proto
Os diretórios dentro de cmd
vão conter o código específico para cada
app, por exemplo: cmd/api
vai conter a função main da API e a lógica para
a inicialização dela. Já no server
, ficará a implementação do nosso servidor,
com a lógica de negócio. Por fim, o diretório proto
vai armazenar os arquivos .proto
que são utilizados para definir nossa API gRPC.
Definição da API gRPC
A nossa API será um CRD (CRUD sem U) para guardar informações sobre produtos.
Vamos defini-la em um arquivo .proto
que ficará em proto/product/v1/product.proto:
mkdir -p proto/product/v1
touch proto/product/v1/product.proto
proto/product/v1/product.proto:
syntax = "proto3";
package product.v1;
option go_package = "github.com/rschio/tutorialgrpc/gen/product/v1";
message Product {
string id = 1;
string name = 2;
double price = 3;
}
message AddProductRequest {
string name = 1;
double price = 2;
}
message AddProductResponse {
string product_id = 1;
}
message DeleteProductRequest {
string product_id = 1;
}
message DeleteProductResponse {
Product product = 1;
}
message ListProductsRequest {
}
message ListProductsResponse {
repeated Product products = 1;
}
service ProductService {
rpc AddProduct(AddProductRequest) returns (AddProductResponse) {}
rpc DeleteProduct(DeleteProductRequest) returns (DeleteProductResponse) {}
rpc ListProducts(ListProductsRequest) returns (ListProductsResponse) {}
}
Ok, vamos entender o que esse arquivo faz. Definimos que a sintaxe utilizada
pelo nosso arquivo é proto3
(existem outras, como proto2
) e definimos o nome do pacote
proto
.
Em go_package
colocamos que os arquivos Go, que serão gerados a partir desse
arquivo proto, ficarão no caminho especificado. Lembre que github.com/rschio/tutorialgrpc
é
o nome do Go mod definido anteriormente (rschio
é o meu usuário, substitua pelo seu).
Em message Product
definimos um tipo (ou objeto) chamado Product e que tem os
campos id
, name
e price
. Os números que aparecem na frente de cada campo
são a ordem em que os campos serão codificados na mensagem, na prática o que precisamos
fazer é escolher um número diferente para cada campo de uma mensagem.
Em service ProductService
definimos a nossa API com as seguintes funções/métodos/endpoints:
AddProduct
, DeleteProduct
e ListProduct
, e cada função recebe um tipo que é o request
e um que é a response.
Gerando código Go a partir do arquivo .proto
Para gerar o código Go vamos utilizar o buf
:
buf mod init -o proto
Esse comando cria o arquivo buf.yaml
(módulo buf) dentro do diretório proto
.
Em seguida, definimos quais plugins queremos que o buf utilize para gerar código, existem plugins para diversas linguagens.
touch proto/buf.gen.yaml
proto/buf.gen.yaml:
version: v1
plugins:
- name: go
out: gen/
opt:
- paths=source_relative
- name: go-grpc
out: gen/
opt:
- paths=source_relative
Definimos os plugins a serem utilizados: go
e go-grpc
, go
vai gerar os tipos que
definimos no arquivo .proto
e go-grpc
vai gerar o serviço. Definimos também que
esses arquivos ficarão no diretório gen
e a opção paths=source_relative
diz que a
estrutura de diretórios deve ser relativa à origem, então os arquivos gerados ficarão
em gen/product/v1
ao invés de gen/github.com/rschio/tutorialgrpc/gen/product/v1
.
Vamos criar um Makefile para automatizar a geração de código:
touch Makefile
Makefile:
.PHONY: proto
proto:
buf generate --template proto/buf.gen.yaml proto
Por fim, geramos o código:
make proto
Com isso finalizamos a parte de definição da API e podemos ver o código gerado
no diretório gen/product/v1
.
Neste ponto do tutorial seu projeto deve estar com a seguinte estrutura:
.
├── cmd
│ ├── api
│ └── gateway
├── gen
│ └── product
│ └── v1
│ ├── product_grpc.pb.go
│ └── product.pb.go
├── go.mod
├── Makefile
├── proto
│ ├── buf.gen.yaml
│ ├── buf.yaml
│ └── product
│ └── v1
│ └── product.proto
└── server
Implementando o servidor gRPC
Crie o arquivo cmd/api/main.go
:
touch cmd/api/main.go
Vamos inicializar a nossa API:
cmd/api/main.go:
package main
import (
"fmt"
"log"
"net"
"os"
v1 "github.com/rschio/tutorialgrpc/gen/product/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
func run() error {
grpcServer := grpc.NewServer()
srv := new(v1.UnimplementedProductServiceServer)
v1.RegisterProductServiceServer(grpcServer, srv)
reflection.Register(grpcServer)
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
l, err := net.Listen("tcp", ":"+port)
if err != nil {
return fmt.Errorf("failed to listen to port %s", port)
}
defer l.Close()
return grpcServer.Serve(l)
}
Na função run
nós criamos um servidor gRPC e um servidor que implementa
um ProductServiceServer
, no caso é o v1.UnimplementedProductServiceServer
que
retorna o erro Unimplemented
em todos os requests.
Na linha seguinte dizemos que o nosso servidor gRPC deve utilizar o ProductServiceServer
e na linha seguinte registramos também o reflection
que é um serviço que descreve
a nossa API (ele não é necessário, porém ajuda nos testes e também serve como um
tipo de documentação).
Em seguida definimos a porta que o servidor vai escutar, e o inicializamos.
Execute go mod tidy
para buscar e baixar as dependências.
Adicione as seguintes linhas ao final do seu Makefile:
Makefile:
.PHONY: runapi
runapi:
cd cmd/api && go run .
Execute make runapi
e em outro terminal execute:
grpcurl -plaintext -d '{"name": "foo", "price": 10.20}' localhost:8080 product.v1.ProductService/AddProduct
A seguinte resposta deve ser obtida:
ERROR:
Code: Unimplemented
Message: method AddProduct not implemented
Bom, se deu tudo certo até aqui, você já tem um servidor gRPC rodando. Todos os endpoints dele retornam erro? Sim, mas tá rodado haha.
Implementando a lógica de negócio
Nesta parte nós vamos de fato implementar as funções da API.
Como esse tutorial não é sobre a linguagem Go em si, eu não vou detalhar as funções, apenas as partes que tem relação com gRPC, caso você tenha alguma dúvida sobre algo que eu não mencionei, deixe nos comentários que eu tento responder.
Crie o arquivo server/server.go
:
touch server/server.go
server/server.go:
package server
import (
"context"
"sync"
"github.com/google/uuid"
v1 "github.com/rschio/tutorialgrpc/gen/product/v1"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type Server struct {
mu sync.Mutex
products map[string]v1.Product
v1.UnimplementedProductServiceServer
}
func New() *Server {
return &Server{
products: make(map[string]v1.Product),
}
}
func (s *Server) AddProduct(ctx context.Context, req *v1.AddProductRequest) (*v1.AddProductResponse, error) {
p := v1.Product{
Id: uuid.New().String(),
Name: req.Name,
Price: req.Price,
}
s.mu.Lock()
s.products[p.Id] = p
s.mu.Unlock()
return &v1.AddProductResponse{ProductId: p.Id}, nil
}
func (s *Server) DeleteProduct(ctx context.Context, req *v1.DeleteProductRequest) (*v1.DeleteProductResponse, error) {
s.mu.Lock()
defer s.mu.Unlock()
p, ok := s.products[req.ProductId]
if !ok {
return nil, status.Errorf(codes.NotFound, "product with ID: %q does not exists", req.ProductId)
}
delete(s.products, req.ProductId)
return &v1.DeleteProductResponse{Product: &p}, nil
}
func (s *Server) ListProducts(ctx context.Context, req *v1.ListProductsRequest) (*v1.ListProductsResponse, error) {
s.mu.Lock()
defer s.mu.Unlock()
ps := make([]*v1.Product, 0, len(s.products))
for id := range s.products {
p := s.products[id]
ps = append(ps, &p)
}
return &v1.ListProductsResponse{Products: ps}, nil
}
Aqui nós definimos o tipo Server
que satisfaz a interface ProductServiceServer
definida no arquivo gen/product/v1/product_grpc.pb.go
:
// ProductServiceServer is the server API for ProductService service.
// All implementations must embed UnimplementedProductServiceServer
// for forward compatibility
type ProductServiceServer interface {
AddProduct(context.Context, *AddProductRequest) (*AddProductResponse, error)
DeleteProduct(context.Context, *DeleteProductRequest) (*DeleteProductResponse, error)
ListProducts(context.Context, *ListProductsRequest) (*ListProductsResponse, error)
mustEmbedUnimplementedProductServiceServer()
}
Para armazenar os produtos utilizamos um map[string]v1.Product
e implementamos
uma lógica simples de Add, Delete e List.
Agora edite o arquivo cmd/api/main.go
para que ele utilize o novo servidor.
cmd/api/main.go:
--- a/cmd/api/main.go
+++ b/cmd/api/main.go
@@ -7,6 +7,7 @@ import (
"os"
v1 "github.com/rschio/tutorialgrpc/gen/product/v1"
+ "github.com/rschio/tutorialgrpc/server"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
@@ -19,7 +20,7 @@ func main() {
func run() error {
grpcServer := grpc.NewServer()
- srv := new(v1.UnimplementedProductServiceServer)
+ srv := server.New()
v1.RegisterProductServiceServer(grpcServer, srv)
reflection.Register(grpcServer)
Execute go mod tidy
para sincronizar as dependências.
Agora vamos rodar nossa API novamente: make runapi
e em outro terminal vamos
testar se ela está funcionando:
grpcurl -plaintext -d '{"name": "foo", "price": 10.20}' localhost:8080 product.v1.ProductService/AddProduct
grpcurl -plaintext -d '{"name": "bar", "price": 5.10}' localhost:8080 product.v1.ProductService/AddProduct
grpcurl -plaintext -d '{}' localhost:8080 product.v1.ProductService/ListProducts
grpcurl -plaintext -d '{"product_id": "<ID do foo>"}' localhost:8080 product.v1.ProductService/DeleteProduct
grpcurl -plaintext -d '{}' localhost:8080 product.v1.ProductService/ListProducts
O resultado deve ser algo do tipo:
{
"productId": "8b8e27b0-03bc-49eb-bcda-b551a07116da"
}
----
{
"productId": "732b1840-c724-43df-9276-c786dcc41566"
}
----
{
"products": [
{
"id": "8b8e27b0-03bc-49eb-bcda-b551a07116da",
"name": "foo",
"price": 10.2
},
{
"id": "732b1840-c724-43df-9276-c786dcc41566",
"name": "bar",
"price": 5.1
}
]
}
----
{
"product": {
"id": "8b8e27b0-03bc-49eb-bcda-b551a07116da",
"name": "foo",
"price": 10.2
}
}
----
{
"products": [
{
"id": "732b1840-c724-43df-9276-c786dcc41566",
"name": "bar",
"price": 5.1
}
]
}
Com isso, temos um servidor gRPC 100% funcional. Neste ponto, a estrutura do seu projeto deve estar assim:
.
├── cmd
│ ├── api
│ │ └── main.go
│ └── gateway
├── gen
│ └── product
│ └── v1
│ ├── product_grpc.pb.go
│ └── product.pb.go
├── go.mod
├── go.sum
├── Makefile
├── proto
│ ├── buf.gen.yaml
│ ├── buf.yaml
│ └── product
│ └── v1
│ └── product.proto
└── server
└── server.go
Transformando gRPC em REST
Ok, gRPC é muito legal, mas muitas vezes precisamos criar APIs que qualquer pessoa consiga consumir sem muito trabalho. A maioria das pessoas não está acostumada a consumir APIs gRPC, mas sim REST, usando o curl ou o Postman.
Para transformar nossa API em REST, vamos utilizar o gRPC-Gateway que é um proxy reverso. Ele vai transformar uma requisição REST em gRPC e a resposta gRPC em REST.
Para adicionar o gateway adicione o plugin grpc-gateway
ao final do
arquivo proto/buf.gen.yaml
:
proto/buf.gen.yaml:
- name: grpc-gateway
out: gen/
opt:
- paths=source_relative
- generate_unbound_methods=true
Ao final arquivo proto/buf.yaml
adicione a dependência necessária para definir a
API rest no arquivo .proto.
proto/buf.yaml:
deps:
- buf.build/googleapis/googleapis
Execute buf mod update proto
para baixar a dependência.
Agora em proto/product/v1/product.proto
vamos definir o formato da API REST:
proto/product/v1/product.proto:
--- a/proto/product/v1/product.proto
+++ b/proto/product/v1/product.proto
@@ -3,6 +3,8 @@ package product.v1;
option go_package = "github.com/rschio/tutorialgrpc/gen/product/v1";
+import "google/api/annotations.proto";
+
message Product {
string id = 1;
string name = 2;
@@ -34,7 +36,20 @@ message ListProductsResponse {
}
service ProductService {
- rpc AddProduct(AddProductRequest) returns (AddProductResponse) {}
- rpc DeleteProduct(DeleteProductRequest) returns (DeleteProductResponse) {}
- rpc ListProducts(ListProductsRequest) returns (ListProductsResponse) {}
+ rpc AddProduct(AddProductRequest) returns (AddProductResponse) {
+ option (google.api.http) = {
+ post: "/api/v1/add"
+ body: "*"
+ };
+ }
+ rpc DeleteProduct(DeleteProductRequest) returns (DeleteProductResponse) {
+ option (google.api.http) = {
+ delete: "/api/v1/delete/{product_id}"
+ };
+ }
+ rpc ListProducts(ListProductsRequest) returns (ListProductsResponse) {
+ option (google.api.http) = {
+ get: "/api/v1/list"
+ };
+ }
}
Aqui nós importamos a dependência previamente instalada e utilizamos em cada método para definir o formato da nossa API REST.
Em AddProduct
definimos que é um método do tipo POST, na rota /api/v1/add
e que deve utilizar o body
para pegar os argumentos do request.
Já em DeleteProduct
, pegamos o argumento product_id
na rota da API.
Agora execute make proto
para gerar o código do gateway e regerar os códigos
gRPCs, isso vai criar o arquivo gen/product/v1/prodcut.pb.gw.go
.
Uma vez que temos a API REST definida podemos criar o servidor do gateway:
touch cmd/gateway/main.go
cmd/gateway/main.go:
package main
import (
"context"
"flag"
"log"
"net/http"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
v1 "github.com/rschio/tutorialgrpc/gen/product/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
var (
port = flag.String("port", "8081", "port is the port this server will use")
endpoint = flag.String("endpoint", "localhost:8080",
"endpoint is the gRPC server's address")
)
func main() {
flag.Parse()
if err := run(); err != nil {
log.Fatal(err)
}
}
func run() error {
ctx := context.Background()
mux := runtime.NewServeMux()
opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
err := v1.RegisterProductServiceHandlerFromEndpoint(ctx, mux, *endpoint, opts)
if err != nil {
return err
}
return http.ListenAndServe(":"+*port, mux)
}
Execute go mod tidy
.
Nesse arquivo definimos a porta em que o gateway vai rodar e o endpoint do nosso servidor gRPC (o qual ele vai traduzir de gRPC para REST).
Mais embaixo, criamos um muxer grpc-gateway que tem a função de decidir qual função vai lidar com qual rota da API.
Depois utilizamos a opção de credenciais inseguras, pois nossa API não tem um certificado TLS (não utilize essa opção em produção).
Na linha de baixo registramos o handler que traduz as requisições e em seguida iniciamos o server.
Adicione o seguinte ao Makefile
:
Makefile:
.PHONY: rungateway
rungateway:
cd cmd/gateway && go run .
Nossa API está pronta, agora só falta testar!
Testando a API REST
Para utilizar a API REST precisamos rodar os dois servidores: api
e gateway
.
Então para os testes vamos utilizar 3 terminais:
Terminal 1: make runapi
Terminal 2: make rungateway
Terminal 3:
curl -X POST -d '{"name": "foo", "price": 10.20}' localhost:8081/api/v1/add
curl -X POST -d '{"name": "bar", "price": 5.10}' localhost:8081/api/v1/add
curl localhost:8081/api/v1/list
curl -X DELETE "localhost:8081/api/v1/delete/<ID do foo>"
curl localhost:8081/api/v1/list
O resultado deve ser parecido com isso:
{"productId":"99b93c07-028a-4584-b0b4-6abee1a72f4b"}
----
{"productId":"95ad91fc-1ec7-4103-9fd3-9f9158f90660"}
----
{"products":[{"id":"99b93c07-028a-4584-b0b4-6abee1a72f4b", "name":"foo", "price":10.2}, {"id":"95ad91fc-1ec7-4103-9fd3-9f9158f90660", "name":"bar", "price":5.1}]}
----
{"product":{"id":"99b93c07-028a-4584-b0b4-6abee1a72f4b", "name":"foo", "price":10.2}}
----
{"products":[{"id":"95ad91fc-1ec7-4103-9fd3-9f9158f90660", "name":"bar", "price":5.1}]}