DEV Community

Cover image for How to Use gRPC in Elixir
Alex Koutmos for AppSignal

Posted on • Originally published at blog.appsignal.com

How to Use gRPC in Elixir

In today's post, we'll learn what gRPC is, when you should reach for such a tool, and some of the pros and cons of using it. After going over an introduction of gRPC, we'll dive right into a sample application where we'll build an Elixir backend API powered by gRPC.

Let's jump right in!

What Is gRPC and How Does It Work?

gRPC is a framework used to enable a remote procedure call (RPC) style of communication. RPC is a style of system communication where a client can directly invoke exposed methods on a server. From the client's perspective, it feels no different than making a call to a local function or method as long as you provide the applicable parameters and handle
the return type appropriately. gRPC facilitates this communication by providing 2 things for you:

  1. A client library which can be used to invoke allowed procedures
  2. A consistent data serialization standard via Protocol Buffers

Let's break these two items down so we can appreciate the inner workings of gRPC. When creating a gRPC server, you'll have to define what procedures are invokable from the client, what inputs they accept, and what outputs they return. This interface specification is what allows client libraries (generally called gRPC stubs) to be automatically generated for various languages and runtimes as the contract for the remote procedure call is explicitly defined. This gRPC client
library can then communicate with the server using Protocol Buffers. Protocol Buffers provide a mechanism for serializing and deserializing the payloads (both request and response) so that you can operate on the data with types native to your language. The diagram available in the gRPC documentation can help visualize this interaction:

gRPC Diagram

One thing that we haven't discussed yet is how data is transmitted between the client and the server. For this, gRPC leans on the HTTP/2 protocol. By using HTTP/2 as the underlying protocol, gRPC is able to support features such as bi-directional data streaming and several other features that are not available in HTTP/1.1.

When Would You Use gRPC Over REST/GraphQL?

The obvious question that may come to mind is: "How does gRPC compare to REST/GraphQL, and when do I use one over the other?".

In general, if you plan to use gRPC for a frontend application, there are a couple of caveats that you need to keep in mind. In order to serialize and deserialize Protocol Buffer payloads from your Javascript application, you'll have to leverage grpc-web. In addition, you'll also need to run a proxy on the backend (the
default supported gRPC proxy is Envoy) since browsers cannot talk directly to gRPC servers. Depending on your resources and time constraints, this may be a show stopper; in which case, REST and GraphQL will do just fine.

If frontend application communication is not a requirement, and instead what you require is inter-microservice communication from within your service cluster, then the barrier to entry for gRPC gets lowered considerably. At the time of writing, gRPC currently has client libraries and tooling for most mainstream languages including Elixir, Python,
C++, Go, Ruby, to name a few. I would argue that the barrier to entry for consuming a RESTful API is still much lower than consuming a gRPC service given that all you need for the former is an HTTP client, which is baked into most languages and runtimes these days.

On the other hand, if you are willing to make the investment, you do get the added benefits of having your responses and requests checked against the Protocol Buffer specification that is used for code generation. This, in turn, provides you with some guarantees as to what you can expect on the client-side and the server-side. This guarantee and introspection is also something that you get with GraphQL when you define your server schemas. An added benefit of GraphQL over gRPC is that you are able to dynamically request embedded properties from within your schema depending on the query that you make to your backend server.

Like most things in the software engineering field, the technology you choose will largely depend on your application. Below are my personal TL;DR rules of thumb regarding the various technologies:

gRPC:

  • Use: When communicating between microservices in my service cluster or if performance is a requirement
  • Don't use: When I need to transmit data from the browser to the backend

GraphQL:

  • Use: When I need to aggregate data from multiple microservices for the purposes of streamlining frontend development, or if my frontend data requirements are dynamic
  • Don't use: When communicating between microservices in my service cluster, or if my API needs to be used by the lowest common denominator of consumers

REST:

  • Use: When I need to put something together quickly or if I need to cater to the lowest common denominator of consumers
  • Don't use: If I require any kind of type checking or if I want to reduce payload sizes over the wire

Experimenting With gRPC in Elixir

With all the theory out of the way, it's time to get our hands dirty and experiment with wiring up a gRPC server. In order to keep us focused on the gRPC experience, we'll opt for having a backend powered by an Agent versus an actual database, but all of the concepts should be easily transferable to an application backed by Postgres, for example. Our
gRPC application will be a simple user management service where we can create and fetch users. After creating our Elixir service, we'll interact with it via grpcurl, which is effectively cURL, but for gRPC. If at any point you get stuck or require the source code, feel free to check it out on GitHub. With all that being said, let's dive right in!

Before creating our Elixir project, there are a couple of things that we require on our machine in order to properly develop and test our application. We'll first need to install protoc so that .proto files can be compiled appropriately. If you are on an OSX machine, you can run brew install protobuf, otherwise, see instructions specific to your platform. Now, with protoc available on your machine, you'll also want to install grpcurl so that you can interact with the application. Once again, if you are on an OSX machine, you can run brew install grpcurl, otherwise, check for instructions specific to your platform.

Lastly, you'll want to run mix escript.install hex protobuf and ensure that protoc-gen-elixir script is available on your path (if you use ASDF as your runtime version manager, this requires running asdf reshim elixir). With all that boilerplate done, you can run mix new sample_app --sup to get a new application started.

Once inside your sample application directory, you'll want to update your mix.exs file to include our gRPC related dependencies. For this application, we will be leveraging https://github.com/elixir-grpc/grpc and
https://github.com/tony612/protobuf-elixir. In order to bring these two dependencies into your project, ensure that your deps/0 function looks like this:

defp deps do
  [
    {:grpc, "~> 0.5.0-beta"},
    {:cowlib, "~> 2.8.0", hex: :grpc_cowlib, override: true}
  ]
end

With that in place, run mix deps.get from the terminal to pull down all necessary project dependencies. Next, you'll want to create a configuration file at config/config.exs with the following content:

use Mix.Config

# Configures Elixir's Logger
config :logger, :console, format: "$time $metadata[$level] $message\n"

config :grpc, start_server: true

Next, we'll want to create the required Protocol Buffer definitions for our application. The Protocol Buffer specification is fairly large and we'll only be using a small subset of it to keep things simple. Create a file sample_app.proto at the root of your project with the following content:

syntax = "proto3";

package sample_app;

service User {
  rpc Create (CreateRequest) returns (UserReply) {}
  rpc Get (GetRequest) returns (UserReply) {}
}

message UserReply {
  int32 id = 1;
  string first_name = 2;
  string last_name = 3;
  int32 age = 4;
}

message CreateRequest {
  string first_name = 1;
  string last_name = 2;
  int32 age = 3;
}

message GetRequest {
  int32 id = 1;
}

As you can see, our Protocol Buffer definition is fairly straightforward and easy to read. We define a service that exposes two RPC methods—Create and Get. We also define the types that each of those RPC calls takes as input and returns as a result. With the sample_app.proto file in place, we'll want to open up a terminal and run the following:

$ protoc --elixir_out=plugins=grpc:./lib sample_app.proto

You'll notice that this command produces a file lib/sample_app.pb.ex with several modules within it. If you look carefully, you'll see that the code that was generated is the Elixir representation of the sample_app.proto file that we wrote. It contains all of the types that we defined along with the RPC method definitions. With our auto-generated
code in place, let's get to work on the actual RPC handlers.

As previously mentioned, we'll be using an Agent to persist state across gRPC calls instead of a database, for the sake of simplicity. Our agent will have the ability to look up users via their ID, and will also be able to create new users. Create a file lib/user_db.ex with the following code which provides that functionality:

defmodule UserDB do
  use Agent

  def start_link(_) do
    Agent.start_link(
      fn ->
        {%{}, 1}
      end,
      name: __MODULE__
    )
  end

  def add_user(user) do
    Agent.get_and_update(__MODULE__, fn {users_map, next_id} ->
      updated_users_map = Map.put(users_map, next_id, user)

      {Map.put(user, :id, next_id), {updated_users_map, next_id + 1}}
    end)
  end

  def get_user(id) do
    Agent.get(__MODULE__, fn {users_map, _next_id} ->
      Map.get(users_map, id)
    end)
  end
end

With that in place, we can create our RPC handlers for creating and getting users. Create a file lib/sample_app.ex with the following content:

defmodule SampleApp.Endpoint do
  use GRPC.Endpoint

  intercept GRPC.Logger.Server
  run SampleApp.User.Server
end

defmodule SampleApp.User.Server do
  use GRPC.Server, service: SampleApp.User.Service

  def create(request, _stream) do
    new_user =
      UserDB.add_user(%{
        first_name: request.first_name,
        last_name: request.last_name,
        age: request.age
      })

    SampleApp.UserReply.new(new_user)
  end

  def get(request, _stream) do
    user = UserDB.get_user(request.id)

    if user == nil do
      raise GRPC.RPCError, status: :not_found
    else
      SampleApp.UserReply.new(user)
    end
  end
end

Our file defines two modules. The SampleApp.Endpoint module defines the gRPC server and provides the handler module to service requests. The SampleApp.User.Server module contains the actual implementations of the two RPC calls that we defined. You'll notice that for each of the handlers, we provide the correct return type (as defined in our Protocol Buffer file). When we encounter an error (in this case, looking up a user that doesn't exist), we raise a GRPC.RPCError with the appropriate status code.

All that is left now is to start up our Agent and our gRPC server, and we're good to go. Open up lib/sample_app/application.ex and ensure that your process children list looks like this:

children = [
  UserDB,
  {GRPC.Server.Supervisor, {SampleApp.Endpoint, 50051}}
]

With that in place, you should be able to run mix grpc.server from the terminal to start your gRPC server. In another terminal session (and from within the project directory), you should be able to use grpcurl commands to interact with your application:

$ grpcurl -plaintext -proto sample_app.proto -d '{"first_name": "Bob", "last_name": "Smith", "age": 40}' localhost:50051 sample_app.User.Create
{
  "id": 1,
  "firstName": "Bob",
  "lastName": "Smith",
  "age": 40
}

$ grpcurl -plaintext -proto sample_app.proto -d '{"id": 1}' localhost:50051 sample_app.User.Get
{
  "firstName": "Bob",
  "lastName": "Smith",
  "age": 40
}

$ grpcurl -plaintext -proto sample_app.proto -d '{"id": 2}' localhost:50051 sample_app.User.Get
ERROR:
  Code: NotFound
  Message: Some requested entity (e.g., file or directory) was not found

Conclusion

Thanks for sticking with me to the end. Hopefully, you learned a thing or two about gRPC and how to go about using it within an Elixir application. If you would like to learn more about gRPC or any of the tools that I mentioned, I suggest going through the following resources:

Guest author Alex Koutmos is a Senior Software Engineer who writes backends in Elixir, frontends in VueJS and deploys his apps using Kubernetes. When he is not programming or blogging he is wrenching on his 1976 Datsun 280z.

P.S. If you'd like to read Elixir Alchemy posts as soon as they get off the press, subscribe to our Elixir Alchemy newsletter and never miss a single post!

Top comments (0)