gRPC proto files Best Practices

gRPC proto files Best Practices

This article is a mash-up of different sources cited in references and Pixelmatic guidelines aimed to serve as coding and style guides for proto files in the context of gRPC services definitions.

Coding and style guides

In short, the philosophy of the guides is:

  • Be consistent
  • Reuse the well-known types from Google
  • But keep separation as needed to avoid breaking changes

Standard file formatting

  • Keep the line length to 80 characters.
  • Prefer the use of double quotes for strings.

File structure

Files should be named lower_snake_case.proto

All files should be ordered in the following manner:

  1. License header (if applicable)
  2. File overview
  3. Syntax
  4. Package
  5. Imports (sorted)
  6. File options
  7. Service (1 service per file)
  8. Messages (sorted in the same order of the RPCs and grouped under a comment// region $RPCNAME)
service Foo {
    rpc Bar(BarRequest) returns (BarResponse);
}

// region Bar
    message BarRequest {
        // ...
    }

    message BarResponse {
        // ...
    }

Note: // region comments are collapsible in Visual Code.

Packages

Package names should be in lowercase. Package names should have unique names based on the project name and possibly based on the path of the file containing the protocol buffer type definitions.

Message and field names

Use CamelCase (with an initial capital) for message names – for example, SongServerRequest. Use underscore_separated_names for field names (including oneof field and extension names) – for example, song_name.

message SongServerRequest {
  optional string song_name = 1;
}

Using this naming convention for field names gives you accessors like the following:

C++:
  const string& song_name() { ... }
  void set_song_name(const string& x) { ... }

Java:
  public String getSongName() { ... }
  public Builder setSongName(String v) { ... }

If your field name contains a number, the number should appear after the letter instead of after the underscore. For example, use song_name1 instead of song_name_1

Optional fields

Consider the following proto3 message which defines the field bar:

*message* Foo {  
    int32 bar = 1; 
}

With this definition, it is impossible to check whether bar has been set to 0 or if no value has been set since the default value of int32 fields is 0.

To allow you to make the difference, use:

message Foo {
  optional int32 bar = 1;
}

This exposes hasBar() and clearBar() (depending on the language) methods in the generated code.

Repeated fields

Use pluralized names for repeated fields.

  repeated string keys = 1;
  ...
  repeated MyMessage accounts = 17;

Oneof

Oneof is a wonderful example where a protobuf language feature helps to make gRPC APIs more intuitive. As an example, imagine we have a service method where users are able to change their profile picture, either from an URL or by uploading their own (small) image. Instead of doing this

// Either set image_url or image_data. Setting both will result in an error.
message ChangeProfilePictureRequest {
  string image_url = 1;
  bytes image_data = 2;
}

we can define the desired behavior directly into the message with oneof

message ChangeProfilePictureRequest {
  oneof image {
    string url = 1;
    bytes data = 2;
  }
}

Not only is that much clearer for API consumers, but it is also easier to check which field has been set in the generated code. Keep in mind that oneof also allows that none of the fields has been set, meaning there is no need to introduce a separate none field if the oneof should be optional.

Flexible Data

If you do not know in advance the nature of the data you are going to receive from your consumer, we suggest you use the well know type Struct. For example:

message StructTest {
  google.protobuf.Struct data = 1;
}

Enums

Use CamelCase (with an initial capital) for enum type names and CAPITALS_WITH_UNDERSCORES for value names:

enum FooBar {
  FOO_BAR_UNSPECIFIED = 0;
  FOO_BAR_FIRST_VALUE = 1;
  FOO_BAR_SECOND_VALUE = 2;
}
  • Each enum value should end with a semicolon, not a comma. Prefer prefixing enum values instead of surrounding them in an enclosing message. The zero value enum should have the suffix UNSPECIFIED.
  • Note that names of enum entries must be unique in the whole package. Defining a second completely unrelated enum with an entry existing in the first will result in an error message because of how enums in C and C++ are implemented. To avoid this, prefix the enum entries with the enum name. Some code generators (ex. for C#) will remove these prefixes automatically so that the resulting code looks "clean" again.

Services

If your .proto defines an RPC service, you should use CamelCase (with an initial capital) for both the service name and any RPC method names:

service FooService {
  rpc GetSomething(GetSomethingRequest) returns (GetSomethingResponse);
  rpc ListSomething(ListSomethingRequest) returns (ListSomethingResponse);
}

Separate request and response messages

We recommend that you create a separate message for each request and response. Name them {MethodName}Request and {MethodName}Reponse. This allows you to modify request and response messages for a single service method without introducing accidental changes to other methods. It is tempting to re-use messages and simply ignore fields that aren't needed. With time, this will result in a mess, since it isn't obvious what the API expects. Exceptions to this rule are usually made when returning a single, well-defined entity or when returning an empty message.

service BookService {
  rpc CreateBook(Book) returns (Book); // don't do this
  rpc CreateBook(Book) returns (google.protobuf.Empty); // instead do this
  rpc ListBooks(ListBooksRequest) returns (ListBooksResponse); // this is OK
  rpc GetBook(GetBookRequest) returns (Book); // this is also OK
  rpc DeleteBook(DeleteBookRequest) returns (google.protobuf.Empty); // this is also OK
}

Use the error system

Do not return a simple confirmation to an endpoint that does not need to return any other data, use google.protobuf.Empty and trigger an error if needed.

// Don't
service BookService {
  rpc DeleteBook(DeleteBookRequest) returns (DeleteBookResponse); 
}

message DeleteBookResponse {
  bool deleted = 1;
}

// Do 
service BookService {
  rpc DeleteBook(DeleteBookRequest) returns (google.protobuf.Empty); 
}

Don’t reinvent the wheel

Use and abuse of the well-known types:

https://developers.google.com/protocol-buffers/docs/reference/google.protobuf

Examples:

Don’t have one field in a struct influence the meaning of another

Protocol buffer messages usually have multiple fields. These fields should always be independent of each other — you shouldn’t have one field influence the semantic meaning of another.

// don't do this!
message Foo {
  int64 timestamp = 1;
  bool timestampIsCreated; // true means timestamp is created time,
                           // false means that it is updated time

This causes confusion — the client now needs to have special logic to know how to interpret one field based on another. Instead, use multiple fields, or use the protobuf “oneof” feature.

// better, but still not ideal because the fields are mutually
// exclusive - only one will be set
message Foo {
  int64 createdTimestamp;
  int64 updatedTimestamp;
}// this is ideal; one will be set, and that is enforced by protobuf
message Foo {
  oneof timestamp {
    int64 created;
    int64 updated;
  }
}

Linting

We use protolint to lint the proto files. Please download and install protolint from https://github.com/yoheimuta/protolint/releases.

Git pre-commit hook

You can also enforce protolint to execute automatically before committing code using pre-commit. Install pre-commit, add a file .pre-commit-config.yaml, and add this content to it:

 repos:
  - repo: https://github.com/yoheimuta/protolint
    rev: master
    hooks:
      - id: protolint

Then run: pre-commit install in the repo.

Extra recommendations

References

Did you find this article valuable?

Support Sonny Alves Dias by becoming a sponsor. Any amount is appreciated!