Building APIs with gRPC: Continued

Ratros Y.
Google Cloud - Community
10 min readFeb 25, 2019

--

This document discusses how to further develop the gRPC API service created in Building APIs with gRPC. It introduces:

  • Common patterns for implementing CREATE, LIST, UPDATE, and DELETE methods in gRPC API services
  • Streaming in gRPC API services
  • Common patterns for error handling and pagination in gRPC API services

The document is a part of the Build API Services: A Beginner’s Guide tutorial series.

Note: This tutorial uses Python 3. OpenAPI generator, of course, supports a variety of programming languages.

About the API service

In this tutorial you will develop the simple one-endpoint gRPC API service created earlier into a full-fledged photo album service where users can create (upload), list, get, and delete their photos. The service provides the following methods (endpoints):

Before you begin

  1. Get started with Building APIs with gRPC.
  2. Download the source code. Open grpc/photo_album.

Understanding the code

As introduced in Building APIs with gRPC, the photo album gRPC API service in this tutorial is built from a Protocol Buffers specification file, example.proto. The specification contains the input (request) and output (response) Protocol Buffers message types for the API service, and service definitions that associates the inputs and outputs together. You can then use Protocol Buffers compiler to compile the specification into Python data classes, server-side artifacts, and client-side artifacts; these artifacts could help you build your gRPC API service and its client libraries.

Resources and their fields

This gRPC API service features two resources: User and Photo. User is the parent resource of Photo.

The resource name of User is of the format //myapiservice.com/users/USER-ID. User features 3 fields:

The resource name of Photo is of the format //myapiservice.com/users/USER-ID/photos/PHOTO-ID. Photo features 3 fields:

Note that for simplicity reasons, Protocol Buffers does not have built-in support for reserved and required fields. All the fields are optional. Developers must validate the data themselves on the server side (and client side, if necessary).

Reusing Protocol Buffers message types

example.proto imports three message types from the protobuf package:

import “google/protobuf/empty.proto”;
import “google/protobuf/timestamp.proto”;
import “google/protobuf/field_mask.proto”;

These message types help you implement common patterns (more specifically, empty messages, time points and field masks) in a gRPC API service. Google offers a variety of Protocol Buffers message types for day-to-day use cases; they are widely used across Google API services. For more information, see the protobuf and api-common-protos GitHub projects.

Of course, you can also import message types from your own projects into a .proto file. To import messages types, for example, from my_project/my_dependencies/messages.proto, simply write import “my_project/my_dependencies/messages.proto” at the beginning of your .proto file.

CREATE methods

example.proto specifies two CREATE methods, CreateUser and CreatePhoto:

CreateUser and CreatePhoto takes User and CreatePhotoRequest as inputs and outputs User and Photo. CreatePhotoRequest specifies two fields, parent and photo:

  • parent is the resource name of Photo’s parent resource, User. The method refers to this value to create a Photo for a specific User.
  • photo is the Photo resource to create.

The two methods will be compiled into CreateUser and CreatePhoto in the server-side and client-side artifacts. Override the server-side artifact to create the methods in the API service, as seen in server.py (/grpc/photo_album/server/server.py):

Call the client-side artifact to connect to the method via client libraries, as seen in client.py (/grpc/photo_album/client/client.py):

Recommended Practices for CREATE methods

CREATE methods usually take the resource to create as input. If the resource is a child of another resource, the input message type should also have a parent parameter for the resource name of its parent resource(s). Also, if your API service supports custom resource identifier, add the resource ID in your message type.

As discussed in Designing APIs and earlier sections, the name fields of resources are always reserved; clients should not be able to declare custom identifiers via name fields in the resource. Instead, add resource ID as a separate field in the input, and generate the full resource name on the server side.

As discussed in Designing APIs, CREATE methods are non-idempotent by nature; you should check for duplicate resources wherever possible.

Additionally, CREATE methods should output the newly created resource instead of a status message (“Resource created.”) so as to help API consumers easier perform subsequent operations on the new resource. This is especially crucial when your resources have reserved or optional fields.

DELETE methods

example.proto specifies one DELETE method, DeletePhoto:

The input is DeletePhotoRequest and the output is Empty (an empty message). It specifies only one field, name:

  • name is the resource name of the Photo to delete.

The method will be compiled into DeletePhoto in the server-side and client-side artifacts. Override the server-side artifact to create the method in the API service, as seen in server.py (/grpc/photo_album/server/server.py):

Call the client-side artifact to connect to the method via client libraries, as seen in client.py (/grpc/photo_album/client/client.py):

Recommended Practices for DELETE methods

DELETE methods are non-idempotent as well. However, different from CREATE methods, calling DELETE methods repeatedly by mistake has few side effects: except for the first one, all the calls will fail as the resource has been removed at the first attempt.

If you would like to specify additional parameters for DELETE methods, add them in the input message type. DELETE methods should always return an Empty message; however, if your API service has a retention policy on deleted resources, consider returning the deleted resource instead.

UPDATE methods

example.proto specifies one UPDATE method, UpdateUser.

UpdateUser takes UpdateUserRequest and returns User. UpdateUserRequest specifies three fields, name, user, and mask:

  • name is the resource name of the User to update.
  • user is the updated User resource.
  • mask is a field mask.

Field mask is a standard pattern for updating resources, which is widely adopted in Google APIs. It essentially specifies a collection of paths (fields) to update in UPDATE methods, allowing API services to modify only the specified fields and leave the other fields untouched. The workflow is as follows:

The method will be compiled into UpdateUser in the server-side and client-side artifacts. Override the server-side artifact to create the method in the API service, as seen in server.py (/grpc/photo_album/server/server.py):

And call the client-side artifact to connect to the method via client libraries, as seen in client.py (/grpc/photo_album/client/client.py):

Best Practices for UPDATE methods

Same as CREATE and DELETE methods, UPDATE methods are also non-idempotent. Fortunately, it is, generally speaking, OK to accept duplicate calls to UPDATE methods; all of them will succeed but the resource stays the same, provided that there are no concurrency problems.

If you would like to specify additional parameters for UPDATE methods, add them in the input message type. You should return the updated resource in UPDATE methods.

LIST methods and pagination

example.proto specifies one LIST method, ListPhotos:

ListPhotos takes ListPhotosRequest as input and returns ListPhotosResponse. ListPhotosRequest specifies 3 fields: parent, order_by, and page_token. ListPhotosResponse specifies 2 fields: a collection of Photos, and next_page_token.

  • parent is the resource name of the parent resource (User). ListPhotos uses this value to retrieve photos of a specific user.
  • order_by is, as its name implies, the order of Photos in the result.
  • page_token enables pagination in the LIST method. For more information, take a look at Designing APIs: Design Patterns: Pagination.

The method will be compiled into ListPhotos in the server-side and client-side artifacts. Override the server-side artifact to create the method in the API service, as seen in server.py (/grpc/photo_album/server/server.py):

Note that server.py keeps all the pagination states exclusively on the server-side.

Call the client-side artifact to connect to the method via client libraries, as seen in client.py (/grpc/photo_album/client/client.py). Users may manually request the next page using next_page_token, though it is highly recommended that you provide a wrapper method that automates the process, preferably in the form of an iterator.

Best Practices for LIST methods

In most cases, you should implement pagination in all the LIST methods of your API service. Also, consider granting clients finer control over LIST methods via additional parameters such as order_by and max_results.

LIST methods are idempotent.

Streaming

Support for streaming is one of the major advantages gRPC API services have over their HTTP RESTful counterparts; it provides a much more natural and idiomatic experience for developers and clients, compared to BATCH endpoints in HTTP RESTful API services. example.proto have two methods with streaming enabled: UploadPhoto, an one-directional (client to server) streaming method, and StreamPhotos, a bi-directional streaming method. gRPC also supports one-directional server-to-client streaming.

Methods with the stream keyword have streaming enabled. To create an one-directional client-to-server streaming method, for example, mark the output (response) message type with the stream keyword; and one-directional server-to-client streaming method the input (request) message type. When both the input and output message types have the keyword, the method becomes a bi-directional streaming endpoint.

UploadPhoto

UploadPhoto takes a stream of PhotoDataBlock as input returns an Empty message. It is a supplementary endpoint to CreatePhoto, enabling clients to upload binary image data block by block to the server. Clients first call CreatePhoto to create the Photo resource, then call UploadPhoto to transfer the data (you may want to add some helper methods in client libraries to help automate the process). It is highly recommended that gRPC API service developers adopt this pattern for processing binary data, as gRPC has a limit on the size of messages (4MB by default). PhotoDataBlock has four fields: name, data_block, data_block_hash, and data_hash.

name is the resource name of the Photo to upload. data_block is a block of binary data. data_block_hash and data_hash, as their names imply, are the hashes of the data block and the complete binary data respectively; they help verify the integrity of the data. The server-side verifies first every data block using data_block_hash, merges all the blocks into the complete file, and verifies it again using data_hash.

The method will be compiled into UploadPhoto in the server-side and client-side artifacts. Override the server-side artifact to create the method in the API service, as seen in server.py (/grpc/photo_album/server/server.py). Since UploadPhoto features a client-to-server one-directional stream, function UploadPhoto in server.py takes an iterator as an input (request_iterator). If helpful, think of it as a regular Python list of UploadPhotoRequest objects; gRPC simply loops through it to get all the requests. Also, the system handles all the streaming complications automatically for you.

Call the client-side artifact to connect to the method via client libraries, as seen in client.py (/grpc/photo_album/client/client.py). To stream from the client-end, build an iterable (PhotoDataBlockRequestIterable) and pass it to the client-side artifact:

Confused about iterator, iterable, and iteration in Python? This StackOverflow answer may help.

More about one-directional streaming methods

gRPC preserves order in a stream. Thus, to assemble the image from the blocks, simply concatenate them in the order of arrival.

Both ends of a stream may choose to terminate the stream prematurely. In this tutorial, the client ends the stream automatically when the iterator throws the StopIteration exception, and the server will cancel the stream if an incoming PhotoDataBlock is corrupted. Prepare for the exceptions accordingly.

Consider adding helpers methods in the client libraries to help with the streaming.

create_and_upload_photo, for example, is a helper method that creates and uploads a photo in one run. It takes the path to the image as input and hides the iteration specifics completely from clients.

StreamPhoto

StreamPhotos takes a stream of GetPhotoRequest and returns a stream of Photo. The method is essentially GetPhoto running in automatic batch mode, enabling clients to retrieve a large number of Photos without having to repeatedly call the GetPhoto method. Basically, for every GetPhotoRequest send in the stream, the API service returns a Photo.

The method will be compiled into StreamPhotos in the server-side and client-side artifacts. Override the server-side artifact to create the method in the API service, as seen in server.py (/grpc/photo_album/server/server.py). Similar to UploadPhoto, StreamPhotos takes an iterator (request_iterator) as input, which represents a stream of GetPhotoRequest messages; the server loops through the iterator and pass the requests to the GetPhoto method one by one.

Note that the function returns a generator using the yield keyword; gRPC calls the generator to prepare responses in the server-to-client stream.

Confused about generators in Python? This Wiki page from the Python Foundation may help.

Call the client-side artifact to connect to the method via client libraries, as seen in client.py (/grpc/photo_album/client/client.py). To stream from the client-side, build an iterable (PhotoDataBlockRequestIterable) and pass it to the client-end stub. Since this is a bi-directional streaming method, it returns an iterator; loop through it to get the Photos.

More about bi-directional streaming methods

The client-to-server and server-to-client streams of a bi-directional streaming method work independently. In this tutorial, the two streams appear synchronized: the server returns one Photo in the server-to-client stream for every GetPhotoRequest in the client-to-server stream, in the order of arrival; however, they do not have to. For instance, you may write a method where a client uploads one file and downloads another at the same time.

Error handling

To throw an error from the server-end or the client-end, set up the gRPC context with an error code and details, then return an Empty response:

To catch an error in the client-end and the server-end with Python gRPC packages, look out for the grpc.RpcError exception, the base exception for all gRPC exceptions. You can extract the error code and details from the error object:

gRPC provides a collection of preset error codes. It is recommended that you use them wherever possible:

CUSTOM error codes are reserved for custom use cases; gRPC itself will never generate these error codes.

Running the code locally

Go to /grpc/photo_album/server/ and Run server.py in the background:

python server.py

The server listens at localhost:8080. Use the client to connect to the server; go to /grpc/photo_album/client and run the following Python script:

import client
client = client.ExamplePhotoServiceClient()
client.create_user(display_name='John Smith', email='user@example.com')

You should see the following outputs:

User created.
name: "//myapiservice.com/users/0947a5a52fa3464da0cee1d9a3a22c8e"
display_name: "John Smith"
email: "user@example.com"

--

--