My Next API — gRPC | RESTful

Probably you are feeling that a RESTful API is not the right choice for your next project or your current use-case is hard to fit into the resource definitions or maybe you are trying to improve the network performance for your request because it’s a key component in your microservice architecture… Are these your thoughts? Maybe this post is for you…

REST — The default choice

Most of the time, when we face a new project that involves a microservice architecture where communication between components is required, we automatically think about a RESTful APIs service, which is great because REST is an architectural style that provides a lot of flexibility and defines a set of rules to be used for creating web service applications. The REST architectural style describes six constraints: Uniform Interface, Stateless, Cacheable, Client-Server, Layered System, Code on Demand (optional).

Another good characteristic of REST is that is Resource-Based where the individual resources are identified in the requests using Uniform Resource Identifiers (URIs). The resources themselves are conceptually separate from the representations that are returned to the client, which means the server does send some XML or JSON that represents the records. When the client retrieves the representation of the resource, it has enough information to modify or delete the resource on the server, if it has the appropriate permissions to do so.

But, REST has some limitations for some use-cases where actions instead of resources are the main goal, an API contract between micro-services is required, operations are difficult to model into resources, streaming is needed, or high performance in terms of network communication is an essential key where JSON is not an optimal choice.

If you are looking for more information about how to implement s RESTful API you can check the below post…

gRPC

gRPC is a modern Remote Procedure Call (RPC) framework developed by Google. It can efficiently connect services with pluggable support for load balancing, tracing, health checking and authentication. It is also applicable in the last mile of distributed computing to connect devices, mobile applications to back-end services.

Currently, gRPC is used mainly for internal services which are not exposed directly to the world that’s why in case you need to consume a gRPC service from a web application or from a language not supported then gRPC offers a REST API Gateway (you will lose most of the benefits of gRPC, but if you need access from an existing service you could do it without re-implementing the service). The gRPC gateway plugin generates a full-fledged REST API server with a reverse proxy and Swagger documentation.

Differences between gRPC and REST

Payload

One of the biggest differences is the format of the payload, while REST messages typically contain JSON, gRPC on the other hand accepts and returns Protobuf messages.

Transfer Protocol

REST depends heavily on HTTP (usually HTTP 1.1) and the request-response model. On the other hand, gRPC uses the newer HTTP/2 protocol. There are several problems that plague HTTP 1.1 that HTTP/2 fixes.

Conceptual Model

REST is very successful these days but most implementations don’t fully adhere to the REST philosophy and use only a subset of its principles because could be actually quite challenging to translate the business logic and the operations into the strict REST world.

On the other side, gRPC has services with clear interfaces and structured messages for requests and responses which translates directly from programming language concepts like interfaces, functions, methods, and data structures.

Where to use gRPC

  1. Microservices: gRPC shines as a way to connect servers in service-oriented environments.
  2. Client-Server Applications: gRPC works just as well in client-server applications, where the client application runs on a desktop or mobile devices using HTTP/2 which improves the latency and network utilization.
  3. Integrations and APIs: gRPC is also a way to offer APIs over the internet for integrating applications with services from third-party providers.
  4. Real-time streaming: When real-time communication is a requirement, gRPC’s ability to manage bidirectional streaming allows your system to send and receive messages in real-time without waiting for Unary client-response communication.
  5. Low-power low-bandwidth networks: gRPC’s use of serialized Protobuf messages offers light-weight messaging, greater efficiency, and speed for bandwidth-constrained, low-power networks.

Practice Time — Example Project

Well, after checking some details about gRPC let’s create a dummy project to understand the required (basic) code to start a gRPC API project. Below you can check the structure:

grpc-python-example
├── client.py
├── definitions
│ ├── builds
│ │ ├── __init__.py
│ │ ├── service_pb2_grpc.py
│ │ └── service_pb2.py
│ ├── __init__.py
│ └── service.proto
├── README.md
├── requirements.txt
├── server.py
└── test.py

First, let’s create a folder, a virtual environment and install the required libraries:

mkdir grpc-python-example
cd
grpc-python-example
virtualenv --python=python3.8 .venv
source .venv/bin/activate
pip install --upgrade pip
pip install grpcio grpcio-tools googleapis-common-protos

Then, the first elements we need to create are the services and the messages using Protobuf syntax.

mkdir definitions
touch service.proto

For this simple project, we will create tickets and will receive the expected day the ticket should be ready depending on the story points argument. In this case, the message is like the resource in a REST API and the service defines the actions in your service (like an endpoint + method in REST).

// service.protosyntax = "proto3";

message Null {}

message Ticket {
string name = 1;
string description = 2;
uint32 story_points = 3;
}

message Confirmation {
string expected_dateline = 1;
}

service TestService {
rpc Health(Null) returns (Null);
rpc AddTicket(Ticket) returns (Confirmation);
}

After the creation of all the .proto files your application requires, it’s time to compile those files and generate the classes. To generate the Python code you need to run the protocol buffer compiler protoc on the .proto file.

The Protocol Compiler is invoked as follows:

python -m grpc_tools.protoc -I definitions/ --python_out=definitions/builds/ --grpc_python_out=definitions/builds/ definitions/service.proto

The above command must generate two files inside the definitions/builds folder: service_pb2_grpc.py and service_pb2.py.

Now, it's time to create the service’s server, for that purpose let’s create the file server.py. There we need to add the service logic for those “actions” defined in the .proto definition as well as the creation and execution of the server.

# server.py
# -*- coding: utf-8 -*-

from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timedelta

import grpc

from definitions.builds.service_pb2 import Confirmation
from definitions.builds.service_pb2_grpc import TestServiceServicer, add_TestServiceServicer_to_server


class Service(TestServiceServicer):
def Health(self, request, context):
return request

def AddTicket(self, request, context):
expected_dateline = datetime.utcnow() + timedelta(days=request.story_points)
return Confirmation(expected_dateline=expected_dateline.strftime("%Y-%m-%d %H:%M:%S"))

def execute_server():
server = grpc.server(ThreadPoolExecutor(max_workers=10))
add_TestServiceServicer_to_server(Service(), server)
server.add_insecure_port("[::]:3000")
server.start()

print("The server is up and running...")
server.wait_for_termination()


if __name__ == "__main__":
execute_server()

For the server execution, you can type in your console: python server.py.

OK, let’s try to consume the data (in this case execute the action) by invoking the method AddTicket in the server. For that, we need to create a client application.

# client.py
# -*- coding: utf-8 -*-

import grpc

from definitions.builds.service_pb2 import Null, Ticket
from definitions.builds.service_pb2_grpc import TestServiceStub


def main():
with grpc.insecure_channel("localhost:3000") as channel:
client = TestServiceStub(channel)
client.Health(Null())

confirmation = client.AddTicket(Ticket(
name="SomeTicket",
description="...",
story_points=2
))

print(confirmation.expected_dateline)


if __name__ == "__main__":
main()

After executing the client…

Performance Comparison

Frequently an important key to take into consideration is the throughput of the requests compared to similar frameworks, in this case, would be Flask or FastAPI, that’s why we performed a simple benchmark by sending 2000 requests.

For gRPC, we’ll use the same client.

# For gRPC...
# -*- coding: utf-8 -*-

from time import time

import grpc

from definitions.builds.service_pb2 import Null, Ticket
from definitions.builds.service_pb2_grpc import TestServiceStub


def main():
with grpc.insecure_channel("localhost:3000") as channel:
client = TestServiceStub(channel)
client.Health(Null())
start = time()

for _ in range(2000):
client.AddTicket(Ticket(
name="SomeTicket",
description="...",
story_points=2
))

print(time() - start)


if __name__ == "__main__":
main()

For both Flask and FastAPI, we need to create a simple server with a similar…, let say structure.

For FastAPI…

# fastapi_server.py
# -*- coding: utf-8 -*-

import json
from datetime import datetime, timedelta

import uvicorn
from fastapi import FastAPI
from starlette.requests import Request

app = FastAPI()


@app.get("/health")
def health():
return {"Hello": "World"}


@app.post("/ticket")
async def manage_tickets(request: Request,):
points = json.loads(await request.body()).get("story_points")
expected_dateline = datetime.utcnow() + timedelta(days=points)
return expected_dateline.strftime("%Y-%m-%d %H:%M:%S")


if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=3001)

For Flask…

# flask_server.py
# -*- coding: utf-8 -*-

import json
from datetime import datetime, timedelta

from flask import Flask
from flask import request

app = Flask(__name__)


@app.route("/")
def health():
return "OK"


@app.route("/ticket", methods=["POST"])
def manage_tickets():
points = json.loads(request.data).get("story_points")
expected_dateline = datetime.utcnow() + timedelta(days=points)
return expected_dateline.strftime("%Y-%m-%d %H:%M:%S")


if __name__ == "__main__":
app.run(port=3001)

To execute the servers…

python flask_server.py
python fastapi_server.py

The test client for Flask and FastAPI will be…

# test.py
# -*- coding: utf-8 -*-

import json
from time import time

import urllib3

http = urllib3.PoolManager()


def main():
start = time()

for _ in range(2000):
http.request(
"POST",
"http://localhost:3001/ticket",
headers={"Content-Type": "application/json"},
body=json.dumps({
"name": "x",
"description": "...",
"story_points": 3
})
)

print(time() - start)


if __name__ == "__main__":
main()

Results…

FastAPI                 Flask                   gRPC
2.5365726947784424 3.588320016860962 0.6878361701965332
2.4663445949554443 3.4863481521606445 0.7431104183197021
2.8214125633239746 3.522181749343872 0.719865083694458

As you can see gRPC is by far the best in terms of performance following by FastAPI in second place.

Conclusions

gRPC is a very good framework with a lot of benefits and maybe in the future will become dominant, but for sure REST will be around for a long time. Frequently we find ourselves looking for comparisons, but I think the key is NOT one tool versus another one, the key is to find the best one which helps us to solve better our use-case. The requirements are the key, is there where the decision start.

Well, that’s all for today, hoping this information may be useful for some of you in your next project…

Code repository: https://github.com/alekglez/grpc-python-example.git

Cloud & Solutions & Data Architect | Python Developer | Serverless Advocate