Building APIs with gRPC: Continued
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
, andDELETE
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
- Get started with Building APIs with gRPC.
- 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 ofPhoto
’s parent resource,User
. The method refers to this value to create aPhoto
for a specificUser
.photo
is thePhoto
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 thePhoto
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 theUser
to update.user
is the updatedUser
resource.mask
is a fieldmask
.
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 ofPhotos
in the result.page_token
enables pagination in theLIST
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"