Skip to main content

Command Palette

Search for a command to run...

gRPC - the new REST!

Published
7 min read
gRPC - the new REST!

REST is the ubiquitous way of writing APIs for as long as I can remember. Today I want to introduce you to a new way of writing APIs.

Have you met gRPC?

gRPC is a relatively new way to write APIs and consume them as if you're just calling a function.

  • gRPC originated from RPC (Remote Procedure Call). The client can just call a stubbed method which will invoke the method at the server side
  • all the connection details are abstracted at the client with this stub
  • it doesn't use everyone's favourite JSON to send the data over the wire, but uses a new format called Protocol Buffers (protobuf in short)
  • protobuf is a new way of serializing data in binary format, which cuts down on payload size and readability both

There are lots of pros on using gRPC as the way to write services over REST that I went ahead and created this big table of analysis for you all :)

gRPC vs REST

pivotgRPCREST
payload sizeprotobuf: smaller and binaryjson: bigger in size cuz text
underlying protocolhttp/2http/1
commbidirectional cuz http/2 and asyncone way, client to server only (no server push)
integrationjust call autogenerated methodsuse http clients and painstakingly create the request object manually
api specprotofiles and autogenerated codebug the backend dev, or hope for a swagger link and lastly trial and error
speed20-25 times faster: less payload and less CPU to convert from binary formatslow due to text based payloads and new connection for each request, no multi-plexing available

variants

Did I tell you that streaming is a first class citizen in gRPC due to HTTP/2 protocol. That means there are four types of APIs you can write and use

  1. no streaming at all just like plain REST - it's called unary api in gRPC lingo
  2. client calling once and then server streaming
  3. client streaming and then the server responding once
  4. both client and server streaming continuously

types of api

defining the interface

Everything starts from a proto file, which contains the definition of the APIs (methods) that the server will implement and clients can call.

I am going to implement a sum service that would take two numbers and return the result.

Below are the definition of the request and response types along with Sum service

syntax = 'proto3';

 message SumRequest {
    int32 num1 = 1;
    int32 num2 = 2;
}

message SumResponse {
    int32 sum = 1;
}

service DemoService {
    rpc Sum(SumRequest) returns (SumResponse);
}

Once this is done, just running mvn clean install will generate the request, response and service classes automatically which will be used to add the logic of the API (no sweat!)

If you're following along, then take a peek at the /target folder and you'll find all the autogenerated code there :)

making the API ready

Time to see how absolutely lazy it is to create the API

  • creating a file (of course), DemoServiceImpl.java (cuz that's how Java folks roll)
  • once you create the class, extend it with the auto-generated class of the same name with which you defined the service in the proto file (DemoService)
  • the auto-generated class has methods for the service, like shown below sum
  • it generates the boilerplate code so that you can just add the logic
import io.grpc.stub.StreamObserver;

public class DemoServiceImpl extends DemoServiceGrpc.DemoServiceImplBase {
    @Override
    public void sum(SumRequest request, StreamObserver<SumResponse> responseObserver) {

    }
}

Adding the logic for sum service; simplest logic in the world :P


import io.grpc.stub.StreamObserver;

public class DemoServiceImpl extends DemoServiceGrpc.DemoServiceImplBase {
    @Override
    public void sum(SumRequest request, StreamObserver<SumResponse> responseObserver) {
        int num1 = request.getNum1();
        int num2 = request.getNum2();

        int result = num1 + num2;

        // creating the response payload
        SumResponse response = SumResponse.newBuilder().setSum(result).build();

        // sending the payload
        responseObserver.onNext(response);
        responseObserver.onCompleted();
    }
}
  • you see the response parameter in the method, notice it's of type StreamObserver
  • this means you can call response.onNext() multiple times and finally when you're done you can do response.onCompleted()
  • this shows that even for a normal (unary) api, you can stream the response.

starting the server

With a handful of boilerplate code we can start the server by writing a main program in a separate class. The first line shows creating the server and adding the service to it. A new instance of DemoServiceImpl.

public class Application {
    public static void main(String[] args) throws IOException, InterruptedException {
        Server server = ServerBuilder.forPort(3000)
                .addService(new DemoServiceImpl())
                .build();

        server.start();

        // boring boilerplate here
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            server.shutdown();
        }));
        server.awaitTermination();
    }
}

consuming the service

Consuming the service consists of three parts

creating the channel

This specifies the endpoint and port of the service running. gRPC is language agnostic, which means you can write your service in one language and call it using another. But here I'm just sticking with java.

creating the stub

This is done using the auto-generated classes. Stub can be of two types:

  • blocking or sync
  • non-blocking

calling the service

This is just calling a method on the stub which corresponds to the actual service running


// step 1 - creating the channel
ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 3000)
    .usePlaintext()
    .build();

// step 2 - creating the stub
DemoServiceGrpc.DemoServiceBlockingStub stub = DemoServiceGrpc.newBlockingStub(channel);

// request
SumRequest request = SumRequest.newBuilder()
    .setNum1(10)
    .setNum2(20)
    .build();

// step 3 - call the service
SumResponse response = stub.sum(request);

// print the result
System.out.println("Response from gRPC service: " + response.getSum());
  • all the classes used above are auto-generated, notice the request and response types which are the same that were defined in the proto file
  • all you need to do is to pass the arguments and call the sum method

this is the magic of gRPC, on the client you're calling just a method and it invokes the actual remote implementation

streaming both ways

Let's see how streaming work both ways, and the perfect example of it would be a chat api, where client is sending messages over the same connection and server responding.

adding new definition in the proto file


+   message ChatRequest {
+     string message = 1;
+   }

+   message ChatResponse {
+     string reply = 1;
+  }

   service DemoService {
     rpc Sum(SumRequest) returns (SumResponse);
+     rpc Chat(stream ChatRequest) returns (stream ChatResponse);
   }
  • note the use of stream keyword in the service definition
  • this tells both the client and server that this would be a bidirectional streaming api
  • let's see how this affects the generated code as the signature of the method will be a little different

Do mvn clean compile to update the generated-code

adding the chat method in service

Back in DemoServiceImpl.java, I magically have the chat method signature which looks like this

    @Override
    public StreamObserver<ChatRequest> chat(StreamObserver<ChatResponse> responseObserver) {

    }
  • notice just one argument to the method, as client will be streaming the request, there's no request object, just the response stream available.
  • also see the return type expected out of the chat method, it's of type ChatRequest, interesting...

So let's implement this

As soon as you try to return the StreamObserver<ChatRequest> object, you'd see this code generated by the IDE. All you've to do is now add logic inside the three methods generated for you.

return new StreamObserver<ChatRequest>() {
  @Override
  public void onNext(ChatRequest chatRequest) {
    // this will be called for every client message
    // notice the argument gives you the client request directly
  }

  @Override
  public void onError(Throwable throwable) {
    // when there's an error at client side
  }

  @Override
  public void onCompleted() {
    // client is done!
    // no more messages would be sent
  }
};

Let's add the logic to reply to this client and reply back using the method argument responseObserver

@Override
public StreamObserver<ChatRequest> chat(StreamObserver<ChatResponse> responseObserver) {
  return new StreamObserver<ChatRequest>() {
    @Override
    public void onNext(ChatRequest chatRequest) {
       // extract the message
+     String message = chatRequest.getMessage();

       // create the response
+      ChatResponse response = ChatResponse.newBuilder()
+         .setReply("server says thanks for sending: " + message)
+          .build();

      // send!
+      responseObserver.onNext(response);
    }

    @Override
    public void onError(Throwable throwable) {

    }

    @Override
    public void onCompleted() {
+      responseObserver.onCompleted();
    }
  };
}

calling from client

private void chat() throws InterruptedException {
  DemoServiceGrpc.DemoServiceStub stub = DemoServiceGrpc.newStub(channel);

  StreamObserver<ChatRequest> request = stub.chat(new StreamObserver<ChatResponse>() {
    @Override
    public void onNext(ChatResponse chatResponse) {
      // called everytime server replies
      System.out.println(chatResponse.getReply());
    }

    @Override
    public void onError(Throwable throwable) {

    }

    @Override
    public void onCompleted() {
      // server is done now
    }
  });

  request.onNext(create("1st message from client"));
  request.onNext(create("2nd message from client"));
  request.onNext(create("3rd message from client"));
  request.onNext(create("4th message from client"));

  request.onCompleted();
}
  • a similar style of api that was implemented in the server
  • same construct of onNext and onCompleted

The final output

server says thanks for sending: 1st message from client
server says thanks for sending: 2nd message from client
server says thanks for sending: 3rd message from client
server says thanks for sending: 4th message from client

code

All the code for this post is available here.

T
T.J.4y ago

you're saying REST is only http/1 ? That's not true, the plain text REST protocol is the most futuristic, and no problem working over HTTP/2 or HTTP/3

gRPC is only good use as protocols within datacenter between micro-services, never meant to be called in browser to server and is probably never going to gain support in browsers

A

so gRPC has benefits from performance level due to protobuf encoding; smaller payloads as well as a clear contract which is tangible and not like REST where things are tied together manually (creation of payload, correct URL, query param vs request body)

M

Cognitive complexity is a thing. gRPC is nice and all, but how many engineers out there actually fluent in it just like in REST? And remember that this number won't grow unless REST is deprecated which is not going to happen.

Though, gRPC is nice for microservices communication.

1
A

I'll reply with a follow up on how to integrate it with web, and try to drive the point home that with generated stubs, maintenance becomes so much easier! Stay tuned :)

W
Wei Lun4y ago

gRPC is just too complex.

2
A

isn't it easier, as you have now auto-generated stubs for all the available service methods?

this will make the integration between backend and frontend super tight - same contract is used to generate the code on both ends, no more searching the correct payload or api endpoint :)

W
Wei Lun4y ago

Ankeet Maini yeah it may sounds easy, but the complexity is on the generation of stubs. For examples:

  • Whether commit stubs to source code or not
  • When to regenerate when protobuf change
  • Dealing with protobuf import paths hell
  • How to version correctly

etc etc...

A

what happens if they add support for http3 in the future BOOM, bye bye REST but still it will take time to adapt in the ecosystem

D

This is pretty good. I think as a technology, we should focus on the pros and cons, and why was it introduced. I think it was introduced to keep a single library for each client and server, so the changes are centralized, unlike REST.

Pros:

  1. It is fast
  2. It uses http2 under the hood
  3. Single library for each client and server maintained by google

Cons

  1. Protobuf makes the client bulky
  2. No direct use of JSON, it won't make sense for the client to know the schema
  3. No browser support YET
  4. It is new and growing
  5. Proxy Support, as any application deployed is not just simple client and server so, the proxy or gateway layer should understand the protocol as well

I believe this is a pretty good piece of technology for microservices intercommunication as it is fast and can reduce the latency in communication.

4
A

It has browser support since 2016, if you check the code link mentioned at the end of the page. You can find the web integrated with gRPC service.

bulky-client

  • it doesn't right? It makes you free from manually hand-crafting the http request
  • it generates the stubs only, which know how to connect to the remote service

json

  • there's no json, as the client can directly use the response objects created and use it in app
  • no need to know the format of the serialzed data sent over the wire
2
D

it's just my opinion when I think of grpc, I really appreciate your article.

I just compared with the following scenario of the rest

  1. you don't need any schema
  2. you don't have to stay fixed with a schema from the client side.
  3. any HTTP request you send does not need to have a prevalidated schema

In the case of grpc, you will need a proto file for each grpc service you want to call to the backend. I meant this when I said bulky.

From Browser support I mean, there is no direct web API that can call the grpc. To call the grpc you will have to follow something like the following

client -> rest endpoint -> grpc endpoint

This was the state when I read about this technology and explored it. I will appreciate it if you can add some resources which can contradict the web API issue.

Thanks for a great article. Happy Coding

1
A

Haha, yes I agree there's a bit of ceremony. Generate code and use a grpc-web proxy for the protocol conversions, but overall maintenance wise it will make a lot easier.

1
T
T.J.4y ago
  • No browser support YET
A

there is with a proxy to patch over the differences and even http1 to http2Tomas Y.

https://github.com/improbable-eng/grpc-web/tree/master/go/grpcwebproxy