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}]}

Sobre mim

GitHub LinkedIn